nix_compat/nixcpp/
conf.rs

1use std::{fmt::Display, str::FromStr};
2
3/// Represents configuration as stored in /etc/nix/nix.conf.
4/// This list is not exhaustive, feel free to add more.
5#[derive(Clone, Debug, Default, Eq, PartialEq)]
6pub struct NixConfig<'a> {
7    pub allowed_users: Option<Vec<&'a str>>,
8    pub auto_optimise_store: Option<bool>,
9    pub cores: Option<u64>,
10    pub max_jobs: Option<u64>,
11    pub require_sigs: Option<bool>,
12    pub sandbox: Option<SandboxSetting>,
13    pub sandbox_fallback: Option<bool>,
14    pub substituters: Option<Vec<&'a str>>,
15    pub system_features: Option<Vec<&'a str>>,
16    pub trusted_public_keys: Option<Vec<crate::narinfo::VerifyingKey>>,
17    pub trusted_substituters: Option<Vec<&'a str>>,
18    pub trusted_users: Option<Vec<&'a str>>,
19    pub extra_platforms: Option<Vec<&'a str>>,
20    pub extra_sandbox_paths: Option<Vec<&'a str>>,
21    pub experimental_features: Option<Vec<&'a str>>,
22    pub builders_use_substitutes: Option<bool>,
23}
24
25impl<'a> NixConfig<'a> {
26    /// Parses configuration from a file like `/etc/nix/nix.conf`, returning
27    /// a [NixConfig] with all values contained in there.
28    /// It does not support parsing multiple config files, merging semantics,
29    /// and also does not understand `include` and `!include` statements.
30    pub fn parse(input: &'a str) -> Result<Self, Error> {
31        let mut out = Self::default();
32
33        for line in input.lines() {
34            // strip comments at the end of the line
35            let line = if let Some((line, _comment)) = line.split_once('#') {
36                line
37            } else {
38                line
39            };
40
41            // skip comments and empty lines
42            if line.trim().is_empty() {
43                continue;
44            }
45
46            let (tag, val) = line
47                .split_once('=')
48                .ok_or_else(|| Error::InvalidLine(line.to_string()))?;
49
50            // trim whitespace
51            let tag = tag.trim();
52            let val = val.trim();
53
54            #[inline]
55            fn parse_val<'a>(this: &mut NixConfig<'a>, tag: &str, val: &'a str) -> Option<()> {
56                match tag {
57                    "allowed-users" => {
58                        this.allowed_users = Some(val.split_whitespace().collect());
59                    }
60                    "auto-optimise-store" => {
61                        this.auto_optimise_store = Some(val.parse::<bool>().ok()?);
62                    }
63                    "cores" => {
64                        this.cores = Some(val.parse().ok()?);
65                    }
66                    "max-jobs" => {
67                        this.max_jobs = Some(val.parse().ok()?);
68                    }
69                    "require-sigs" => {
70                        this.require_sigs = Some(val.parse().ok()?);
71                    }
72                    "sandbox" => this.sandbox = Some(val.parse().ok()?),
73                    "sandbox-fallback" => this.sandbox_fallback = Some(val.parse().ok()?),
74                    "substituters" => this.substituters = Some(val.split_whitespace().collect()),
75                    "system-features" => {
76                        this.system_features = Some(val.split_whitespace().collect())
77                    }
78                    "trusted-public-keys" => {
79                        this.trusted_public_keys = Some(
80                            val.split_whitespace()
81                                .map(crate::narinfo::VerifyingKey::parse)
82                                .collect::<Result<Vec<crate::narinfo::VerifyingKey>, _>>()
83                                .ok()?,
84                        )
85                    }
86                    "trusted-substituters" => {
87                        this.trusted_substituters = Some(val.split_whitespace().collect())
88                    }
89                    "trusted-users" => this.trusted_users = Some(val.split_whitespace().collect()),
90                    "extra-platforms" => {
91                        this.extra_platforms = Some(val.split_whitespace().collect())
92                    }
93                    "extra-sandbox-paths" => {
94                        this.extra_sandbox_paths = Some(val.split_whitespace().collect())
95                    }
96                    "experimental-features" => {
97                        this.experimental_features = Some(val.split_whitespace().collect())
98                    }
99                    "builders-use-substitutes" => {
100                        this.builders_use_substitutes = Some(val.parse().ok()?)
101                    }
102                    _ => return None,
103                }
104                Some(())
105            }
106
107            parse_val(&mut out, tag, val)
108                .ok_or_else(|| Error::InvalidValue(tag.to_string(), val.to_string()))?
109        }
110
111        Ok(out)
112    }
113}
114
115#[derive(thiserror::Error, Debug)]
116pub enum Error {
117    #[error("Invalid line: {0}")]
118    InvalidLine(String),
119    #[error("Unrecognized key: {0}")]
120    UnrecognizedKey(String),
121    #[error("Invalid value '{1}' for key '{0}'")]
122    InvalidValue(String, String),
123}
124
125/// Valid values for the Nix 'sandbox' setting
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum SandboxSetting {
128    True,
129    False,
130    Relaxed,
131}
132
133impl Display for SandboxSetting {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            SandboxSetting::True => write!(f, "true"),
137            SandboxSetting::False => write!(f, "false"),
138            SandboxSetting::Relaxed => write!(f, "relaxed"),
139        }
140    }
141}
142
143impl FromStr for SandboxSetting {
144    type Err = &'static str;
145
146    fn from_str(s: &str) -> Result<Self, Self::Err> {
147        Ok(match s {
148            "true" => Self::True,
149            "false" => Self::False,
150            "relaxed" => Self::Relaxed,
151            _ => return Err("invalid value"),
152        })
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::{narinfo::VerifyingKey, nixcpp::conf::SandboxSetting};
159
160    use super::NixConfig;
161
162    #[test]
163    pub fn test_parse() {
164        let config = NixConfig::parse(include_str!("../../testdata/nix.conf")).expect("must parse");
165
166        assert_eq!(
167            NixConfig {
168                allowed_users: Some(vec!["*"]),
169                auto_optimise_store: Some(false),
170                cores: Some(0),
171                max_jobs: Some(8),
172                require_sigs: Some(true),
173                sandbox: Some(SandboxSetting::True),
174                sandbox_fallback: Some(false),
175                substituters: Some(vec!["https://nix-community.cachix.org", "https://cache.nixos.org/"]),
176                system_features: Some(vec!["nixos-test", "benchmark", "big-parallel", "kvm"]),
177                trusted_public_keys: Some(vec![
178                    VerifyingKey::parse("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=")
179                        .expect("failed to parse pubkey"),
180                    VerifyingKey::parse("nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=")
181                        .expect("failed to parse pubkey")
182                ]),
183                trusted_substituters: Some(vec![]),
184                trusted_users: Some(vec!["flokli"]),
185                extra_platforms: Some(vec!["aarch64-linux", "i686-linux"]),
186                extra_sandbox_paths: Some(vec![
187                    "/run/binfmt", "/nix/store/swwyxyqpazzvbwx8bv40z7ih144q841f-qemu-aarch64-binfmt-P-x86_64-unknown-linux-musl"
188                ]),
189                experimental_features: Some(vec!["nix-command"]),
190                builders_use_substitutes: Some(true)
191            },
192            config
193        );
194
195        // parse a config file using some non-space whitespaces, as well as comments right after the lines.
196        // ensure it contains the same data as initially parsed.
197        let other_config = NixConfig::parse(include_str!("../../testdata/other_nix.conf"))
198            .expect("other config must parse");
199
200        assert_eq!(config, other_config);
201    }
202}