pharos-corefile 0.1.0

Corefile lexer and parser for Pharos
Documentation
//! Converts parsed Directives into caller-owned typed configuration.
//!
//! A caller supplies setup functions in Plugin Order and a concrete config type;
//! setup may build an enum generated by plugin.cfg without dynamic Plugin dispatch.
//! Parser tokens are visible only through the temporary setup cursor, never in
//! the returned configuration. One context is shared by every key of a block.

use std::collections::{HashMap, HashSet};

use crate::{Cursor, Error, ServerBlock, ServerBlockKey, Token};

/// A known Directive and its setup function, in Plugin Order.
pub struct Directive<'a, C> {
    pub name: &'a str,
    pub once: bool,
    pub setup: fn(&mut Cursor<'_>, &mut BlockContext<C>) -> Result<(), Error>,
}

/// Typed configuration and listener metadata shared by all keys of one block.
#[derive(Debug)]
pub struct BlockContext<C> {
    keys: Vec<ServerBlockKey>,
    pub config: C,
    /// True when setup has installed a View filter.
    pub view: bool,
    /// Listener addresses, with an empty string denoting all addresses.
    pub listen_addresses: Vec<String>,
}

impl<C> BlockContext<C> {
    /// Normalized keys that share this configuration.
    pub fn keys(&self) -> &[ServerBlockKey] {
        &self.keys
    }
}

/// Run known Directive setup functions in caller-supplied Plugin Order.
pub fn configure<C: Default>(
    blocks: &[ServerBlock],
    registry: &[Directive<'_, C>],
) -> Result<Vec<BlockContext<C>>, Error> {
    let mut configured = Vec::with_capacity(blocks.len());
    for block in blocks {
        for (name, tokens) in &block.directives {
            if tokens.is_empty() {
                return Err(Error {
                    file: String::new(),
                    line: 0,
                    message: format!("Directive '{name}' has no tokens"),
                });
            }
            if !registry.iter().any(|directive| directive.name == name) {
                let token = &tokens[0];
                return Err(Error {
                    file: token.file.clone(),
                    line: token.line,
                    message: format!("Unknown directive '{name}'"),
                });
            }
        }
        let mut context = BlockContext {
            keys: block.normalize_keys()?,
            config: C::default(),
            view: false,
            listen_addresses: vec![String::new()],
        };
        for directive in registry {
            let Some(tokens) = block.directives.get(directive.name) else {
                continue;
            };
            if directive.once
                && let Some(token) = second_occurrence(tokens)
            {
                return Err(Error {
                    file: token.file.clone(),
                    line: token.line,
                    message: format!(
                        "{}: this plugin can only be used once per Server Block",
                        directive.name
                    ),
                });
            }
            let mut cursor = Cursor::new(&tokens[0].file, tokens);
            (directive.setup)(&mut cursor, &mut context)?;
        }
        configured.push(context);
    }
    validate_overlaps(blocks, &configured)?;
    Ok(configured)
}

fn validate_overlaps<C>(
    blocks: &[ServerBlock],
    configured: &[BlockContext<C>],
) -> Result<(), Error> {
    let mut registered: HashSet<(ServerBlockKey, String)> = HashSet::new();
    let mut unbound: HashMap<ServerBlockKey, String> = HashMap::new();
    for (block, context) in blocks.iter().zip(configured) {
        for (key, source) in block.normalized_key_sources()? {
            for address in &context.listen_addresses {
                let exact = registered.contains(&(key.clone(), address.clone()));
                let overlap = if exact {
                    None
                } else if address.is_empty() {
                    unbound.get(&key).map(String::as_str)
                } else if registered.contains(&(key.clone(), String::new())) {
                    Some("")
                } else {
                    None
                };
                let current = format_address(&key, address);
                if exact && !context.view {
                    return Err(Error {
                        file: source.file.clone(),
                        line: source.line,
                        message: format!("cannot serve {current} - it is already defined"),
                    });
                }
                if let Some(previous) = overlap {
                    return Err(Error {
                        file: source.file.clone(),
                        line: source.line,
                        message: format!(
                            "cannot serve {current} - zone overlap listener capacity with {}",
                            format_address(&key, previous)
                        ),
                    });
                }
                if !context.view {
                    registered.insert((key.clone(), address.clone()));
                    unbound.insert(key.clone(), address.clone());
                }
            }
        }
    }
    Ok(())
}

fn format_address(key: &ServerBlockKey, address: &str) -> String {
    let mut value = format!("{}://{}:{}", key.transport, key.domain, key.port);
    if !address.is_empty() {
        value.push_str(&format!(" on {address}"));
    }
    value
}

fn second_occurrence(tokens: &[Token]) -> Option<&Token> {
    let mut nesting = 0usize;
    for (index, token) in tokens.iter().enumerate() {
        if index > 0 && nesting == 0 {
            let previous = &tokens[index - 1];
            if crate::parser::new_line(previous, token) {
                return Some(token);
            }
        }
        match token.text.as_str() {
            "{" => nesting += 1,
            "}" => nesting = nesting.saturating_sub(1),
            _ => {}
        }
    }
    None
}