Skip to main content

pharos_corefile/
parser.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fmt,
4    path::{Component, Path, PathBuf},
5};
6
7use crate::{ServerBlockKey, Token, lex, normalize_key};
8
9/// Parsed Server Block keys and Directive tokens, grouped by Directive name.
10#[derive(Clone, Debug, Default, Eq, PartialEq)]
11pub struct ServerBlock {
12    keys: Vec<String>,
13    key_sources: Vec<Token>,
14    pub directives: BTreeMap<String, Vec<Token>>,
15}
16
17impl ServerBlock {
18    /// The Server Block's original, environment-expanded keys.
19    pub fn keys(&self) -> &[String] {
20        &self.keys
21    }
22
23    /// Apply the DNS default port without changing explicitly specified ports.
24    pub fn set_default_dns_port(&mut self, port: u16) {
25        for key in &mut self.keys {
26            let host = key.strip_prefix("dns://").unwrap_or(key);
27            if host.ends_with(':')
28                || host.contains("://")
29                || host
30                    .rsplit_once(':')
31                    .is_some_and(|(_, port)| port.parse::<i64>().is_ok())
32            {
33                continue;
34            }
35            key.push_str(&format!(":{port}"));
36        }
37    }
38
39    /// Validate and normalize keys, retaining source locations for errors.
40    pub fn normalize_keys(&self) -> Result<Vec<ServerBlockKey>, Error> {
41        Ok(self
42            .normalized_key_sources()?
43            .into_iter()
44            .map(|(key, _)| key)
45            .collect())
46    }
47
48    pub(crate) fn normalized_key_sources(&self) -> Result<Vec<(ServerBlockKey, &Token)>, Error> {
49        let mut normalized = Vec::new();
50        for (key, source) in self.keys.iter().zip(&self.key_sources) {
51            normalized.extend(
52                normalize_key(key)
53                    .map_err(|message| Error {
54                        file: source.file.clone(),
55                        line: source.line,
56                        message,
57                    })?
58                    .into_iter()
59                    .map(|key| (key, source)),
60            );
61        }
62        Ok(normalized)
63    }
64}
65
66/// A Corefile error with the source location of the offending token.
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct Error {
69    pub file: String,
70    pub line: usize,
71    pub message: String,
72}
73
74impl fmt::Display for Error {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "{}:{}: {}", self.file, self.line, self.message)
77    }
78}
79
80impl std::error::Error for Error {}
81
82pub(crate) fn new_line(previous: &Token, current: &Token) -> bool {
83    previous.file != current.file
84        || previous.line + previous.text.matches('\n').count() < current.line
85}
86
87fn clean_path(path: &Path) -> PathBuf {
88    let mut clean = PathBuf::new();
89    for component in path.components() {
90        if component == Component::ParentDir {
91            clean.pop();
92        } else {
93            clean.push(component);
94        }
95    }
96    clean
97}
98
99/// Parse a Corefile without validating Plugin-specific Directive names or arguments.
100pub fn parse(file: &str, input: &str) -> Result<Vec<ServerBlock>, Error> {
101    parse_with_env(file, input, |name| std::env::var(name).ok())
102}
103
104/// Parse using an injectable environment lookup.
105pub fn parse_with_env(
106    file: &str,
107    input: &str,
108    lookup: impl Fn(&str) -> Option<String>,
109) -> Result<Vec<ServerBlock>, Error> {
110    Parser {
111        tokens: lex(file, input),
112        pos: 0,
113        file,
114        lookup: &lookup,
115        snippets: BTreeMap::new(),
116        snippet_expansions: 0,
117        file_expansions: 0,
118    }
119    .parse_all()
120}
121
122struct Parser<'a> {
123    tokens: Vec<Token>,
124    pos: usize,
125    file: &'a str,
126    lookup: &'a dyn Fn(&str) -> Option<String>,
127    snippets: BTreeMap<String, Vec<Token>>,
128    snippet_expansions: usize,
129    file_expansions: usize,
130}
131
132impl Parser<'_> {
133    fn substitute(&self, text: &str) -> Result<String, Error> {
134        let mut result = text.to_owned();
135        for (start, end) in [("{%", "%}"), ("{$", "}")] {
136            let mut seen = BTreeSet::new();
137            while let Some(index) = result.find(start) {
138                let Some(end_offset) = result[index..].find(end) else {
139                    break;
140                };
141                let end_index = index + end_offset;
142                if end_index <= index + start.len() {
143                    break;
144                }
145                let name = &result[index + start.len()..end_index];
146                let value = (self.lookup)(name).unwrap_or_default();
147                let reference = &result[index..end_index + end.len()];
148                // shortcut: bound expansion to 10,000 steps and 16 MiB; upgrade for larger configurations.
149                let occurrences = result.matches(reference).count();
150                let size = result.len() - occurrences * reference.len();
151                if seen.len() >= 10_000
152                    || !seen.insert(result.clone())
153                    || value
154                        .len()
155                        .checked_mul(occurrences)
156                        .and_then(|n| n.checked_add(size))
157                        .is_none_or(|n| n > 16 * 1024 * 1024)
158                {
159                    return Err(self.error("environment expansion limit or cycle"));
160                }
161                result = result.replace(reference, &value);
162            }
163        }
164        Ok(result)
165    }
166    fn error(&self, message: &str) -> Error {
167        let token = self.tokens.get(self.pos).or_else(|| self.tokens.last());
168        Error {
169            file: token.map_or(self.file, |t| &t.file).into(),
170            line: token.map_or(1, |t| t.line),
171            message: message.into(),
172        }
173    }
174
175    fn parse_all(&mut self) -> Result<Vec<ServerBlock>, Error> {
176        let mut blocks = Vec::new();
177        while self.pos < self.tokens.len() {
178            let block = self.block()?;
179            if !block.keys.is_empty() {
180                blocks.push(block);
181            }
182        }
183        Ok(blocks)
184    }
185
186    fn block(&mut self) -> Result<ServerBlock, Error> {
187        let mut block = ServerBlock::default();
188        let mut expecting = false;
189        while self.pos < self.tokens.len() {
190            let text = self.substitute(&self.tokens[self.pos].text)?;
191            if text == "import"
192                && (self.pos == 0 || new_line(&self.tokens[self.pos - 1], &self.tokens[self.pos]))
193            {
194                self.import()?;
195                continue;
196            }
197            if text == "{" {
198                if expecting {
199                    return Err(self.error("expected another Server Block Key"));
200                }
201                break;
202            }
203            if !text.is_empty() {
204                expecting = text.ends_with(',');
205                block
206                    .keys
207                    .push(text.strip_suffix(',').unwrap_or(&text).into());
208                block.key_sources.push(self.tokens[self.pos].clone());
209            }
210            self.pos += 1;
211            if self.pos == self.tokens.len() {
212                if expecting {
213                    return Err(self.error("unexpected end of input"));
214                }
215                return Ok(block);
216            }
217            if !expecting && new_line(&self.tokens[self.pos - 1], &self.tokens[self.pos]) {
218                break;
219            }
220        }
221        if self.pos == self.tokens.len() {
222            return Ok(block);
223        }
224        if block.keys.len() == 1 && block.keys[0].starts_with('(') && block.keys[0].ends_with(')') {
225            let name = block.keys[0][1..block.keys[0].len() - 1].to_owned();
226            if self.tokens[self.pos].text != "{" {
227                return Err(self.error("expected snippet opening brace"));
228            }
229            if self.snippets.contains_key(&name) {
230                return Err(self.error("duplicate snippet"));
231            }
232            self.pos += 1;
233            let start = self.pos;
234            let mut nesting = 1;
235            while self.pos < self.tokens.len() && nesting > 0 {
236                match self.tokens[self.pos].text.as_str() {
237                    "{" => nesting += 1,
238                    "}" => nesting -= 1,
239                    _ => {}
240                }
241                self.pos += 1;
242            }
243            if nesting != 0 {
244                return Err(self.error("expected snippet closing brace"));
245            }
246            let tokens = self.tokens[start..self.pos - 1].to_vec();
247            for (i, token) in tokens.iter().enumerate() {
248                if token.text == "import"
249                    && (i == 0 || new_line(&tokens[i - 1], token))
250                    && let Some(next) = tokens.get(i + 1)
251                    && self.substitute(&next.text)? == name
252                {
253                    return Err(self.error("recursive snippet"));
254                }
255            }
256            self.snippets.insert(name, tokens);
257            return Ok(ServerBlock::default());
258        }
259        let braced = self.tokens[self.pos].text == "{";
260        if braced {
261            self.pos += 1;
262        }
263        while self.pos < self.tokens.len() && self.tokens[self.pos].text != "}" {
264            if self.tokens[self.pos].text == "import" {
265                self.import()?;
266                continue;
267            }
268            self.directive(&mut block)?;
269        }
270        if braced {
271            if self.pos == self.tokens.len() {
272                return Err(self.error("expected closing brace"));
273            }
274            self.pos += 1;
275        } else if self.pos < self.tokens.len() {
276            self.pos += 1;
277        }
278        Ok(block)
279    }
280
281    fn directive(&mut self, block: &mut ServerBlock) -> Result<(), Error> {
282        let name = self.substitute(&self.tokens[self.pos].text)?;
283        let mut items = vec![self.tokens[self.pos].clone()];
284        self.pos += 1;
285        let mut nesting = 0usize;
286        while self.pos < self.tokens.len() {
287            let token = &self.tokens[self.pos];
288            if token.text == "{" {
289                nesting += 1;
290            } else if nesting == 0 && new_line(&self.tokens[self.pos - 1], token) {
291                break;
292            } else if token.text == "}" {
293                if nesting == 0 {
294                    return Err(self.error("unexpected closing brace"));
295                }
296                nesting -= 1;
297            } else if token.text == "import" && new_line(&self.tokens[self.pos - 1], token) {
298                self.import()?;
299                continue;
300            }
301            let mut token = token.clone();
302            token.text = self.substitute(&token.text)?;
303            items.push(token);
304            self.pos += 1;
305        }
306        if nesting != 0 {
307            return Err(self.error("unexpected end of input"));
308        }
309        block.directives.entry(name).or_default().extend(items);
310        Ok(())
311    }
312
313    fn import(&mut self) -> Result<(), Error> {
314        let start = self.pos;
315        let Some(arg) = self.tokens.get(start + 1) else {
316            return Err(self.error("import requires an argument"));
317        };
318        if arg.file != self.tokens[start].file
319            || arg.line != self.tokens[start].line + self.tokens[start].text.matches('\n').count()
320        {
321            return Err(self.error("import requires an argument"));
322        }
323        let pattern = self.substitute(&arg.text)?;
324        if pattern.is_empty() {
325            return Err(self.error("import requires a non-empty path"));
326        }
327        if let Some(next) = self.tokens.get(start + 2)
328            && next.file == arg.file
329            && next.line == arg.line + arg.text.matches('\n').count()
330        {
331            return Err(self.error("import takes only one argument"));
332        }
333        let imported = if let Some(tokens) = self.snippets.get(&pattern) {
334            self.snippet_expansions += 1;
335            if self.snippet_expansions > 10_000 {
336                return Err(self.error("snippet import limit exceeded"));
337            }
338            tokens.clone()
339        } else {
340            self.file_expansions += 1;
341            if self.file_expansions > 100_000 {
342                return Err(self.error("file import limit exceeded"));
343            }
344            let base = Path::new(&self.tokens[start].file);
345            let base = if base.is_absolute() {
346                base.to_path_buf()
347            } else {
348                std::env::current_dir()
349                    .map_err(|e| self.error(&e.to_string()))?
350                    .join(base)
351            };
352            let base = clean_path(&base);
353            let path = if Path::new(&pattern).is_absolute() {
354                pattern.clone().into()
355            } else {
356                clean_path(&base.parent().unwrap_or(Path::new("/")).join(&pattern))
357            };
358            let glob_pattern = path.to_string_lossy();
359            if glob_pattern.matches('*').count() > 1
360                || glob_pattern.matches('?').count() > 1
361                || (glob_pattern.contains('[') && glob_pattern.contains(']'))
362            {
363                return Err(self.error("too many import wildcards"));
364            }
365            let mut translated = String::new();
366            let mut chars = glob_pattern.chars();
367            while let Some(ch) = chars.next() {
368                if ch == '\\' {
369                    let Some(escaped) = chars.next().filter(|c| *c != '/') else {
370                        return Err(self.error("invalid import pattern"));
371                    };
372                    translated.push_str(&glob::Pattern::escape(&escaped.to_string()));
373                } else {
374                    translated.push(ch);
375                }
376            }
377            let paths = glob::glob(&translated).map_err(|e| self.error(&e.to_string()))?;
378            let mut imported = Vec::new();
379            let mut count = 0;
380            for path in paths.flatten() {
381                count += 1;
382                let contents = std::fs::read(&path).map_err(|e| self.error(&e.to_string()))?;
383                imported.extend(lex(
384                    &clean_path(&path).to_string_lossy(),
385                    &crate::lexer::decode(&contents),
386                ));
387            }
388            if count == 0 && !glob_pattern.contains(['*', '?', '[', ']']) {
389                return Err(self.error("import file not found"));
390            }
391            imported
392        };
393        self.tokens.splice(start..start + 2, imported);
394        Ok(())
395    }
396}