Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-purgatory/src/kconfig/parse.rs

//! **Reading Kconfig assignments** — the one place a `.config` line becomes data.
//!
//! Deliberately a parser and nothing else. It does not know what a symbol means,
//! whether it is hardening, or whether it should be there; those are
//! [`crate::subtract`]'s questions and keeping them out of here is what lets the
//! set arithmetic in [`super::effective`] be pure.

use std::fmt;

/// What a Kconfig symbol is set to.
///
/// `No` is a first-class value rather than an absence because Kconfig spells it
/// explicitly (`# CONFIG_X is not set`) and a fragment that says so is making a
/// **claim** — the vetoes in `nuked.config` are exactly this. Treating "not set"
/// as "absent" would erase the difference between "we refused it" and "nobody
/// mentioned it", which is the difference `DROPPED` and `ADDED` are built on.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Value {
    /// `CONFIG_X=y`
    Yes,
    /// `CONFIG_X=m`
    Module,
    /// `# CONFIG_X is not set`
    No,
    /// `CONFIG_X="…"`
    Str(String),
    /// `CONFIG_X=42`, `CONFIG_X=0x40`
    Num(String),
}

impl Value {
    /// Is the feature actually present in the kernel with this value?
    ///
    /// `m` counts: a module is compiled, shipped and loadable, so it is surface.
    #[must_use]
    pub fn is_enabled(&self) -> bool {
        matches!(self, Value::Yes | Value::Module)
    }
}

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Value::Yes => write!(f, "y"),
            Value::Module => write!(f, "m"),
            Value::No => write!(f, "n"),
            Value::Str(s) => write!(f, "{s:?}"),
            Value::Num(n) => write!(f, "{n}"),
        }
    }
}

/// One assignment: `CONFIG_SECCOMP=y`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Directive {
    /// The symbol WITHOUT the `CONFIG_` prefix — stored bare so `CONFIG_X` and
    /// `X` cannot become two entries for one symbol.
    pub symbol: String,
    pub value: Value,
}

impl Directive {
    /// As it would appear in a `.config`.
    #[must_use]
    pub fn render(&self) -> String {
        match &self.value {
            Value::No => format!("# CONFIG_{} is not set", self.symbol),
            v => format!("CONFIG_{}={v}", self.symbol),
        }
    }
}

impl fmt::Display for Directive {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.render())
    }
}

/// Strip a leading `CONFIG_`, so one symbol has one key.
fn bare(sym: &str) -> &str {
    sym.strip_prefix("CONFIG_").unwrap_or(sym)
}

/// Parse ONE line. `None` for blanks, comments that are not `is not set`, and
/// anything unrecognised.
///
/// Unrecognised is `None` rather than an error on purpose: a `.config` carries
/// banner comments and a `# CONFIG_X is not set` line differs from them by shape
/// alone. Silently skipping a *malformed assignment* would be the bad kind of
/// lenience, so the shapes accepted below are exact.
#[must_use]
pub fn parse_line(line: &str) -> Option<Directive> {
    let t = line.trim();
    if t.is_empty() {
        return None;
    }
    if let Some(rest) = t.strip_prefix('#') {
        // `# CONFIG_X is not set` — and nothing else beginning with `#`.
        let rest = rest.trim();
        let sym = rest.strip_suffix("is not set")?.trim();
        let sym = sym.strip_prefix("CONFIG_")?;
        if sym.is_empty() || !is_symbol(sym) {
            return None;
        }
        return Some(Directive {
            symbol: sym.to_string(),
            value: Value::No,
        });
    }
    let (lhs, rhs) = t.split_once('=')?;
    let sym = bare(lhs.trim());
    if sym.is_empty() || !is_symbol(sym) {
        return None;
    }
    let rhs = rhs.trim();
    let value = match rhs {
        "y" => Value::Yes,
        "m" => Value::Module,
        "n" => Value::No,
        _ if rhs.starts_with('"') => Value::Str(rhs.trim_matches('"').to_string()),
        _ if !rhs.is_empty() => Value::Num(rhs.to_string()),
        _ => return None,
    };
    Some(Directive {
        symbol: sym.to_string(),
        value,
    })
}

/// Kconfig symbols are `[A-Z0-9_]`. Checked so a prose line that happens to
/// contain `=` cannot become a directive.
fn is_symbol(s: &str) -> bool {
    !s.is_empty()
        && s.chars()
            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
}

/// Every directive in a fragment or a `.config`, in file order.
#[must_use]
pub fn parse_config(text: &str) -> Vec<Directive> {
    text.lines().filter_map(parse_line).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_four_shapes_a_config_actually_uses() {
        assert_eq!(
            parse_line("CONFIG_SECCOMP=y"),
            Some(Directive {
                symbol: "SECCOMP".into(),
                value: Value::Yes
            })
        );
        assert_eq!(
            parse_line("CONFIG_E1000=m"),
            Some(Directive {
                symbol: "E1000".into(),
                value: Value::Module
            })
        );
        assert_eq!(
            parse_line("# CONFIG_ATA is not set"),
            Some(Directive {
                symbol: "ATA".into(),
                value: Value::No
            })
        );
        assert_eq!(
            parse_line("CONFIG_LSM=\"lockdown,yama\""),
            Some(Directive {
                symbol: "LSM".into(),
                value: Value::Str("lockdown,yama".into())
            })
        );
        assert_eq!(
            parse_line("CONFIG_LOG_BUF_SHIFT=17"),
            Some(Directive {
                symbol: "LOG_BUF_SHIFT".into(),
                value: Value::Num("17".into())
            })
        );
    }

    /// **`n` is a VALUE, not an absence.** The vetoes in `nuked.config` are
    /// claims, and `DROPPED`/`ADDED` are built on the difference between "we
    /// refused it" and "nobody mentioned it".
    #[test]
    fn not_set_is_a_value_and_is_not_enabled() {
        let d = parse_line("# CONFIG_DEBUG_FS is not set").unwrap();
        assert_eq!(d.value, Value::No);
        assert!(!d.value.is_enabled());
        assert!(Value::Yes.is_enabled());
        assert!(
            Value::Module.is_enabled(),
            "a module is compiled, shipped and loadable — surface"
        );
    }

    /// A banner comment must not become a directive, and neither must prose that
    /// happens to contain `=`.
    #[test]
    fn prose_and_banners_are_not_directives() {
        for line in [
            "",
            "   ",
            "#",
            "# Automatically generated file; DO NOT EDIT.",
            "# Linux/x86 6.12.104 Kernel Configuration",
            "# CONFIG_ATA is not set for a reason",
            "# this one mentions is not set but has no CONFIG_",
            "lower_case=y",
            "CONFIG_=y",
            "no equals here",
        ] {
            assert_eq!(parse_line(line), None, "{line:?} must not parse");
        }
    }

    /// `CONFIG_X` and a bare `X` are ONE symbol, or the set arithmetic double-counts.
    #[test]
    fn the_config_prefix_is_stripped_so_one_symbol_is_one_key() {
        assert_eq!(parse_line("CONFIG_SMP=y").unwrap().symbol, "SMP");
        assert_eq!(parse_line("SMP=y").unwrap().symbol, "SMP");
    }

    #[test]
    fn render_round_trips_every_value() {
        for line in [
            "CONFIG_SECCOMP=y",
            "CONFIG_E1000=m",
            "# CONFIG_ATA is not set",
            "CONFIG_LOG_BUF_SHIFT=17",
        ] {
            assert_eq!(parse_line(line).unwrap().render(), line);
        }
    }
}