chrony-confile 0.1.0

A full-featured Rust library for parsing, editing, validating, and serializing chrony configuration files
Documentation
//! Error types for parsing, validation, and value construction.
//!
//! This module defines the three error types used throughout the crate:
//! - [`ParseError`] -- I/O errors, parse errors, and include-level exceeded errors
//! - [`DirectiveError`] -- Errors specific to a single directive (invalid option, value, etc.)
//! - [`ValueError`] -- Errors from constructing out-of-range or invalid values
//!
//! Both [`ParseError`] and [`DirectiveError`] implement [`std::error::Error`] (via `thiserror`),
//! making them compatible with `?` and any error-reporting crate.

use std::{io, path::PathBuf};
use crate::span::Span;

/// Represents errors that can occur during parsing of chrony configuration files.
///
/// This includes I/O errors when reading included files, directive-level parse errors,
/// and configuration-level errors such as exceeding the maximum include depth or
/// encountering lines exceeding the maximum length.
///
/// [`ParseError`] implements [`std::error::Error`] via `thiserror` and supports
/// conversion from [`std::io::Error`].
///
/// # Examples
///
/// ```rust
/// use chrony_confile::ChronyConfig;
///
/// // Malformed input produces a ParseError
/// let result = ChronyConfig::parse("::invalid directive::");
/// assert!(result.is_err());
/// ```
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    #[error("I/O error: {0}")]
    Io(#[from] io::Error),

    /// A directive-level parse error with an optional file name.
    #[error("{inner}")]
    Parse {
        file: Option<PathBuf>,
        #[source]
        inner: DirectiveError,
    },

    /// Maximum include nesting depth was exceeded during config expansion.
    #[error("Maximum include level reached at {file}:{line}")]
    IncludeLevelExceeded { file: PathBuf, line: usize },

    /// A line exceeded the maximum allowed length (2048 bytes).
    #[error("Line too long ({len} bytes) at {}:{line}", file.as_deref().map_or("?", |p| p.to_str().unwrap_or("?")))]
    LineTooLong {
        file: Option<PathBuf>,
        line: usize,
        len: usize,
    },
}

impl PartialEq for ParseError {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Io(a), Self::Io(b)) => a.kind() == b.kind(),
            (
                Self::Parse {
                    file: f1,
                    inner: i1,
                },
                Self::Parse {
                    file: f2,
                    inner: i2,
                },
            ) => f1 == f2 && i1 == i2,
            (
                Self::IncludeLevelExceeded {
                    file: f1,
                    line: l1,
                },
                Self::IncludeLevelExceeded {
                    file: f2,
                    line: l2,
                },
            ) => f1 == f2 && l1 == l2,
            (
                Self::LineTooLong {
                    file: f1,
                    line: l1,
                    len: n1,
                },
                Self::LineTooLong {
                    file: f2,
                    line: l2,
                    len: n2,
                },
            ) => f1 == f2 && l1 == l2 && n1 == n2,
            _ => false,
        }
    }
}

/// Errors specific to a single directive during parsing.
///
/// Covers invalid directive names, invalid options, invalid or out-of-range values,
/// and incorrect argument counts.
///
/// These errors are typically wrapped in [`ParseError::Parse`] when returned from
/// the top-level parsing API.
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum DirectiveError {
    /// The directive name is not a recognised chrony directive.
    #[error("Invalid directive `{name}` at {span}")]
    InvalidDirective { name: String, span: Span },

    /// The option is not valid for the given directive.
    /// The option is not valid for the given directive.
    #[error("Invalid option `{option}` in `{directive}` at {span}")]
    InvalidOption {
        directive: String,
        option: String,
        span: Span,
    },

    /// The argument value could not be parsed as the expected type.
    #[error("Invalid value in `{directive}`: expected {expected}, got `{got}` at {span}")]
    InvalidValue {
        directive: String,
        expected: &'static str,
        got: String,
        span: Span,
    },

    /// A numeric value fell outside the valid range.
    #[error("Value `{value}` out of range (min {min}, max {max}) in `{directive}` at {span}")]
    ValueOutOfRange {
        directive: String,
        value: String,
        min: String,
        max: String,
        span: Span,
    },

    /// Not enough arguments were provided for the directive.
    #[error("Missing arguments in `{directive}`: expected {expected}, got {got} at {span}")]
    MissingArgument {
        directive: String,
        expected: usize,
        got: usize,
        span: Span,
    },

    /// More arguments were provided than the directive expects.
    #[error("Too many arguments in `{directive}`: expected {expected}, got {got} at {span}")]
    TooManyArguments {
        directive: String,
        expected: usize,
        got: usize,
        span: Span,
    },
}

/// Errors from constructing bounded integer values.
///
/// Returned by types like [`PollInterval`](crate::values::PollInterval),
/// [`UdpPort`](crate::values::UdpPort), and [`Stratum`](crate::values::Stratum) when
/// a value is out of range or has an invalid format.
///
/// # Examples
///
/// ```rust
/// use chrony_confile::values::PollInterval;
///
/// // Values outside the valid range produce a ValueError
/// let err = PollInterval::new(100).unwrap_err();
/// assert!(err.to_string().contains("out of range"));
/// ```
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum ValueError {
    /// The value is outside the valid range of the bounded integer type.
    #[error("Value {value} out of range ({min}..{max})")]
    OutOfRange { value: i64, min: i64, max: i64 },

    /// The string could not be parsed as the expected format.
    #[error("Invalid format `{value}`: expected {expected}")]
    InvalidFormat { value: String, expected: &'static str },

    /// The keyword did not match any of the valid options.
    #[error("Invalid keyword `{value}`, expected one of: {}", valid.join(", "))]
    InvalidKeyword {
        value: String,
        valid: &'static [&'static str],
    },
}