use std::collections::{HashMap, HashSet};
use crate::{Cursor, Error, ServerBlock, ServerBlockKey, Token};
pub struct Directive<'a, C> {
pub name: &'a str,
pub once: bool,
pub setup: fn(&mut Cursor<'_>, &mut BlockContext<C>) -> Result<(), Error>,
}
#[derive(Debug)]
pub struct BlockContext<C> {
keys: Vec<ServerBlockKey>,
pub config: C,
pub view: bool,
pub listen_addresses: Vec<String>,
}
impl<C> BlockContext<C> {
pub fn keys(&self) -> &[ServerBlockKey] {
&self.keys
}
}
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
}