Skip to main content

pharos_corefile/
setup.rs

1//! Converts parsed Directives into caller-owned typed configuration.
2//!
3//! A caller supplies setup functions in Plugin Order and a concrete config type;
4//! setup may build an enum generated by plugin.cfg without dynamic Plugin dispatch.
5//! Parser tokens are visible only through the temporary setup cursor, never in
6//! the returned configuration. One context is shared by every key of a block.
7
8use std::collections::{HashMap, HashSet};
9
10use crate::{Cursor, Error, ServerBlock, ServerBlockKey, Token};
11
12/// A known Directive and its setup function, in Plugin Order.
13pub struct Directive<'a, C> {
14    pub name: &'a str,
15    pub once: bool,
16    pub setup: fn(&mut Cursor<'_>, &mut BlockContext<C>) -> Result<(), Error>,
17}
18
19/// Typed configuration and listener metadata shared by all keys of one block.
20#[derive(Debug)]
21pub struct BlockContext<C> {
22    keys: Vec<ServerBlockKey>,
23    pub config: C,
24    /// True when setup has installed a View filter.
25    pub view: bool,
26    /// Listener addresses, with an empty string denoting all addresses.
27    pub listen_addresses: Vec<String>,
28}
29
30impl<C> BlockContext<C> {
31    /// Normalized keys that share this configuration.
32    pub fn keys(&self) -> &[ServerBlockKey] {
33        &self.keys
34    }
35}
36
37/// Run known Directive setup functions in caller-supplied Plugin Order.
38pub fn configure<C: Default>(
39    blocks: &[ServerBlock],
40    registry: &[Directive<'_, C>],
41) -> Result<Vec<BlockContext<C>>, Error> {
42    let mut configured = Vec::with_capacity(blocks.len());
43    for block in blocks {
44        for (name, tokens) in &block.directives {
45            if tokens.is_empty() {
46                return Err(Error {
47                    file: String::new(),
48                    line: 0,
49                    message: format!("Directive '{name}' has no tokens"),
50                });
51            }
52            if !registry.iter().any(|directive| directive.name == name) {
53                let token = &tokens[0];
54                return Err(Error {
55                    file: token.file.clone(),
56                    line: token.line,
57                    message: format!("Unknown directive '{name}'"),
58                });
59            }
60        }
61        let mut context = BlockContext {
62            keys: block.normalize_keys()?,
63            config: C::default(),
64            view: false,
65            listen_addresses: vec![String::new()],
66        };
67        for directive in registry {
68            let Some(tokens) = block.directives.get(directive.name) else {
69                continue;
70            };
71            if directive.once
72                && let Some(token) = second_occurrence(tokens)
73            {
74                return Err(Error {
75                    file: token.file.clone(),
76                    line: token.line,
77                    message: format!(
78                        "{}: this plugin can only be used once per Server Block",
79                        directive.name
80                    ),
81                });
82            }
83            let mut cursor = Cursor::new(&tokens[0].file, tokens);
84            (directive.setup)(&mut cursor, &mut context)?;
85        }
86        configured.push(context);
87    }
88    validate_overlaps(blocks, &configured)?;
89    Ok(configured)
90}
91
92fn validate_overlaps<C>(
93    blocks: &[ServerBlock],
94    configured: &[BlockContext<C>],
95) -> Result<(), Error> {
96    let mut registered: HashSet<(ServerBlockKey, String)> = HashSet::new();
97    let mut unbound: HashMap<ServerBlockKey, String> = HashMap::new();
98    for (block, context) in blocks.iter().zip(configured) {
99        for (key, source) in block.normalized_key_sources()? {
100            for address in &context.listen_addresses {
101                let exact = registered.contains(&(key.clone(), address.clone()));
102                let overlap = if exact {
103                    None
104                } else if address.is_empty() {
105                    unbound.get(&key).map(String::as_str)
106                } else if registered.contains(&(key.clone(), String::new())) {
107                    Some("")
108                } else {
109                    None
110                };
111                let current = format_address(&key, address);
112                if exact && !context.view {
113                    return Err(Error {
114                        file: source.file.clone(),
115                        line: source.line,
116                        message: format!("cannot serve {current} - it is already defined"),
117                    });
118                }
119                if let Some(previous) = overlap {
120                    return Err(Error {
121                        file: source.file.clone(),
122                        line: source.line,
123                        message: format!(
124                            "cannot serve {current} - zone overlap listener capacity with {}",
125                            format_address(&key, previous)
126                        ),
127                    });
128                }
129                if !context.view {
130                    registered.insert((key.clone(), address.clone()));
131                    unbound.insert(key.clone(), address.clone());
132                }
133            }
134        }
135    }
136    Ok(())
137}
138
139fn format_address(key: &ServerBlockKey, address: &str) -> String {
140    let mut value = format!("{}://{}:{}", key.transport, key.domain, key.port);
141    if !address.is_empty() {
142        value.push_str(&format!(" on {address}"));
143    }
144    value
145}
146
147fn second_occurrence(tokens: &[Token]) -> Option<&Token> {
148    let mut nesting = 0usize;
149    for (index, token) in tokens.iter().enumerate() {
150        if index > 0 && nesting == 0 {
151            let previous = &tokens[index - 1];
152            if crate::parser::new_line(previous, token) {
153                return Some(token);
154            }
155        }
156        match token.text.as_str() {
157            "{" => nesting += 1,
158            "}" => nesting = nesting.saturating_sub(1),
159            _ => {}
160        }
161    }
162    None
163}