use std::{fmt::Display, str::FromStr}; /// Represents configuration as stored in /etc/nix/nix.conf. /// This list is not exhaustive, feel free to add more. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct NixConfig<'a> { pub allowed_users: Option<Vec<&'a str>>, pub auto_optimise_store: Option<bool>, pub cores: Option<u64>, pub max_jobs: Option<u64>, pub require_sigs: Option<bool>, pub sandbox: Option<SandboxSetting>, pub sandbox_fallback: Option<bool>, pub substituters: Option<Vec<&'a str>>, pub system_features: Option<Vec<&'a str>>, pub trusted_public_keys: Option<Vec<crate::narinfo::VerifyingKey>>, pub trusted_substituters: Option<Vec<&'a str>>, pub trusted_users: Option<Vec<&'a str>>, pub extra_platforms: Option<Vec<&'a str>>, pub extra_sandbox_paths: Option<Vec<&'a str>>, pub experimental_features: Option<Vec<&'a str>>, pub builders_use_substitutes: Option<bool>, } impl<'a> NixConfig<'a> { /// Parses configuration from a file like `/etc/nix/nix.conf`, returning /// a [NixConfig] with all values contained in there. /// It does not support parsing multiple config files, merging semantics, /// and also does not understand `include` and `!include` statements. pub fn parse(input: &'a str) -> Result<Self, Error> { let mut out = Self::default(); for line in input.lines() { // strip comments at the end of the line let line = if let Some((line, _comment)) = line.split_once('#') { line } else { line }; // skip comments and empty lines if line.trim().is_empty() { continue; } let (tag, val) = line .split_once('=') .ok_or_else(|| Error::InvalidLine(line.to_string()))?; // trim whitespace let tag = tag.trim(); let val = val.trim(); #[inline] fn parse_val<'a>(this: &mut NixConfig<'a>, tag: &str, val: &'a str) -> Option<()> { match tag { "allowed-users" => { this.allowed_users = Some(val.split_whitespace().collect()); } "auto-optimise-store" => { this.auto_optimise_store = Some(val.parse::<bool>().ok()?); } "cores" => { this.cores = Some(val.parse().ok()?); } "max-jobs" => { this.max_jobs = Some(val.parse().ok()?); } "require-sigs" => { this.require_sigs = Some(val.parse().ok()?); } "sandbox" => this.sandbox = Some(val.parse().ok()?), "sandbox-fallback" => this.sandbox_fallback = Some(val.parse().ok()?), "substituters" => this.substituters = Some(val.split_whitespace().collect()), "system-features" => { this.system_features = Some(val.split_whitespace().collect()) } "trusted-public-keys" => { this.trusted_public_keys = Some( val.split_whitespace() .map(crate::narinfo::VerifyingKey::parse) .collect::<Result<Vec<crate::narinfo::VerifyingKey>, _>>() .ok()?, ) } "trusted-substituters" => { this.trusted_substituters = Some(val.split_whitespace().collect()) } "trusted-users" => this.trusted_users = Some(val.split_whitespace().collect()), "extra-platforms" => { this.extra_platforms = Some(val.split_whitespace().collect()) } "extra-sandbox-paths" => { this.extra_sandbox_paths = Some(val.split_whitespace().collect()) } "experimental-features" => { this.experimental_features = Some(val.split_whitespace().collect()) } "builders-use-substitutes" => { this.builders_use_substitutes = Some(val.parse().ok()?) } _ => return None, } Some(()) } parse_val(&mut out, tag, val) .ok_or_else(|| Error::InvalidValue(tag.to_string(), val.to_string()))? } Ok(out) } } #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Invalid line: {0}")] InvalidLine(String), #[error("Unrecognized key: {0}")] UnrecognizedKey(String), #[error("Invalid value '{1}' for key '{0}'")] InvalidValue(String, String), } /// Valid values for the Nix 'sandbox' setting #[derive(Clone, Debug, PartialEq, Eq)] pub enum SandboxSetting { True, False, Relaxed, } impl Display for SandboxSetting { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SandboxSetting::True => write!(f, "true"), SandboxSetting::False => write!(f, "false"), SandboxSetting::Relaxed => write!(f, "relaxed"), } } } impl FromStr for SandboxSetting { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(match s { "true" => Self::True, "false" => Self::False, "relaxed" => Self::Relaxed, _ => return Err("invalid value"), }) } } #[cfg(test)] mod tests { use crate::{narinfo::VerifyingKey, nixcpp::conf::SandboxSetting}; use super::NixConfig; #[test] pub fn test_parse() { let config = NixConfig::parse(include_str!("../../testdata/nix.conf")).expect("must parse"); assert_eq!( NixConfig { allowed_users: Some(vec!["*"]), auto_optimise_store: Some(false), cores: Some(0), max_jobs: Some(8), require_sigs: Some(true), sandbox: Some(SandboxSetting::True), sandbox_fallback: Some(false), substituters: Some(vec!["https://nix-community.cachix.org", "https://cache.nixos.org/"]), system_features: Some(vec!["nixos-test", "benchmark", "big-parallel", "kvm"]), trusted_public_keys: Some(vec![ VerifyingKey::parse("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=") .expect("failed to parse pubkey"), VerifyingKey::parse("nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=") .expect("failed to parse pubkey") ]), trusted_substituters: Some(vec![]), trusted_users: Some(vec!["flokli"]), extra_platforms: Some(vec!["aarch64-linux", "i686-linux"]), extra_sandbox_paths: Some(vec![ "/run/binfmt", "/nix/store/swwyxyqpazzvbwx8bv40z7ih144q841f-qemu-aarch64-binfmt-P-x86_64-unknown-linux-musl" ]), experimental_features: Some(vec!["nix-command"]), builders_use_substitutes: Some(true) }, config ); // parse a config file using some non-space whitespaces, as well as comments right after the lines. // ensure it contains the same data as initially parsed. let other_config = NixConfig::parse(include_str!("../../testdata/other_nix.conf")) .expect("other config must parse"); assert_eq!(config, other_config); } }