chrony-confile 0.1.0

A full-featured Rust library for parsing, editing, validating, and serializing chrony configuration files
Documentation
//! Line normalization and tokenization.
//!
//! This module implements chrony's `CPS_NormalizeLine` logic: stripping whitespace,
//! identifying comment/blank/directive lines, and collapsing internal whitespace.
//! It also provides [`split_word`] and [`count_args`] helpers used by the grammar module.

/// Maximum line length per chrony's MAX_LINE_LENGTH.
pub const MAX_LINE_LENGTH: usize = 2048;

/// Result of normalizing a configuration line.
///
/// After applying chrony's `CPS_NormalizeLine` logic, each raw line is classified as
/// one of these variants.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LineType {
    /// A directive line with its command name and arguments.
    Directive { command: String, args: String },
    /// A comment line (content after the `#`, `;`, `!`, or `%` prefix).
    Comment(String),
    /// An empty or whitespace-only line.
    Blank,
}

/// Normalize a raw configuration line following chrony's CPS_NormalizeLine logic.
///
/// 1. Strip leading whitespace
/// 2. If the first non-whitespace character is `!`, `;`, `#`, or `%`, it is a comment line
/// 3. Collapse internal whitespace to single spaces
/// 4. Strip trailing whitespace
/// 5. Return the classified line type
pub fn normalize_line(line: &str) -> LineType {
    let trimmed = line.trim_start();

    if trimmed.is_empty() {
        return LineType::Blank;
    }

    // Check for comment characters (only when they are the first non-ws char)
    if let Some(first) = trimmed.chars().next()
        && matches!(first, '!' | ';' | '#' | '%')
    {
        let comment = trimmed[1..].trim();
        return LineType::Comment(comment.to_string());
    }

    // Collapse whitespace
    let mut result = String::with_capacity(line.len());
    let mut in_space = false;
    let mut first = true;

    for ch in line.chars() {
        if ch.is_ascii_whitespace() {
            if !first {
                in_space = true;
            }
        } else {
            if in_space {
                result.push(' ');
                in_space = false;
            }
            result.push(ch);
            first = false;
        }
    }

    // Split into command and args at the first space
    if let Some(space_pos) = result.find(' ') {
        let command = result[..space_pos].to_string();
        let args = result[space_pos + 1..].to_string();
        LineType::Directive { command, args }
    } else {
        LineType::Directive {
            command: result,
            args: String::new(),
        }
    }
}

/// Split the first whitespace-delimited word from a string, returning (word, rest).
///
/// This is equivalent to chrony's `CPS_SplitWord` function. Leading whitespace on `rest`
/// is stripped for convenience.
pub fn split_word(input: &str) -> (&str, &str) {
    let trimmed = input.trim_start();
    if trimmed.is_empty() {
        return ("", "");
    }
    match trimmed.find(char::is_whitespace) {
        Some(space) => (&trimmed[..space], trimmed[space..].trim_start()),
        None => (trimmed, ""),
    }
}

/// Count the number of whitespace-delimited arguments in a normalized args string.
pub fn count_args(args: &str) -> usize {
    if args.is_empty() {
        return 0;
    }
    args.split(' ').count()
}