chrony-confile 0.1.0

A full-featured Rust library for parsing, editing, validating, and serializing chrony configuration files
Documentation
//! Structural validation of individual directives.
//!
//! Validates constraints on single directives such as ensuring `minpoll <= maxpoll`
//! for `server` directives and that `dpoll` is negative when PPS rate > 1.

use crate::ast::*;
use crate::error::DirectiveError;

/// Validates structural constraints on individual directives.
///
/// Checks:
/// - `minpoll <= maxpoll` for `server`, `pool`, and `peer` directives
/// - `dpoll` must be negative when PPS rate > 1 for `refclock` directives
/// - `maxsources` in pool must be in valid range (1..=16)
pub fn validate_config(config: &ChronyConfig) -> Vec<DirectiveError> {
    let mut errors = Vec::new();
    for node in &config.nodes {
        if let ConfigNode::Directive(d) = node {
            validate_poll_limits(d, &mut errors);
            validate_refclock_dpoll(d, &mut errors);
            validate_pool_maxsources(d, &mut errors);
        }
    }
    errors
}

fn validate_poll_limits(d: &Directive, errors: &mut Vec<DirectiveError>) {
    let (name, minpoll, maxpoll) = match &d.kind {
        DirectiveKind::Server(c) => ("server", c.minpoll.get(), c.maxpoll.get()),
        DirectiveKind::Pool(c) => ("pool", c.source.minpoll.get(), c.source.maxpoll.get()),
        DirectiveKind::Peer(c) => ("peer", c.minpoll.get(), c.maxpoll.get()),
        DirectiveKind::RefClock(c) => ("refclock", c.poll.get(), c.poll.get()), // refclock has only poll, no minpoll/maxpoll check needed here
        _ => return,
    };
    if minpoll > maxpoll {
        errors.push(DirectiveError::InvalidValue {
            directive: name.into(),
            expected: "minpoll <= maxpoll",
            got: format!("minpoll {minpoll}, maxpoll {maxpoll}"),
            span: d.span.clone(),
        });
    }
}

fn validate_refclock_dpoll(d: &Directive, errors: &mut Vec<DirectiveError>) {
    if let DirectiveKind::RefClock(c) = &d.kind
        && matches!(c.driver, RefClockDriverKind::Pps)
        && c.pps_rate.get() > 1
        && c.dpoll.get() >= 0
    {
        errors.push(DirectiveError::InvalidOption {
            directive: "refclock".into(),
            option: "dpoll must be negative when rate > 1".into(),
            span: d.span.clone(),
        });
    }
}

fn validate_pool_maxsources(d: &Directive, errors: &mut Vec<DirectiveError>) {
    if let DirectiveKind::Pool(c) = &d.kind
        && (c.max_sources < 1 || c.max_sources > 16)
    {
        errors.push(DirectiveError::ValueOutOfRange {
            directive: "pool".into(),
            value: c.max_sources.to_string(),
            min: "1".into(),
            max: "16".into(),
            span: d.span.clone(),
        });
    }
}