Skip to main content

selinux_configfile/
config_file.rs

1//! The [`ConfigFile`] type — the main high-level API for reading, modifying,
2//! validating, and writing SELinux config files.
3
4use std::collections::HashSet;
5
6use crate::error::ValueError;
7use crate::parser;
8use crate::types::{
9    AUTORELABEL_KEY, Line, REQUIRESEUSERS_KEY, SELINUX_KEY, SELINUXTYPE_DEFAULT, SELINUXTYPE_KEY,
10    SETLOCALDEFS_KEY, SelinuxMode,
11};
12
13/// A parsed `/etc/selinux/config` file with format-preserving lines.
14#[derive(Debug, Clone, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct ConfigFile {
17    pub(crate) lines: Vec<Line>,
18}
19
20impl ConfigFile {
21    /// Create an empty config file (no lines at all).
22    pub fn new() -> Self {
23        ConfigFile { lines: Vec::new() }
24    }
25
26    /// Parse a config file string, preserving all formatting.
27    pub fn parse(input: &str) -> Result<Self, crate::error::ParseError> {
28        parser::parse(input)
29    }
30
31    /// Return all lines (comments, blanks, raws, entries) in order.
32    #[must_use]
33    pub fn lines(&self) -> &[Line] {
34        &self.lines
35    }
36
37    /// True when there are no [`Line::Entry`] lines at all.
38    #[must_use]
39    pub fn is_empty(&self) -> bool {
40        !self.lines.iter().any(|l| matches!(l, Line::Entry { .. }))
41    }
42
43    /// Check if a key exists (case-insensitive).
44    #[must_use]
45    pub fn contains(&self, key: &str) -> bool {
46        self.lines.iter().any(|line| match line {
47            Line::Entry { key_raw, .. } => key_raw.eq_ignore_ascii_case(key),
48            _ => false,
49        })
50    }
51
52    /// Generic getter: case-insensitive, last-wins for duplicate keys.
53    #[must_use]
54    pub fn get(&self, key: &str) -> Option<&str> {
55        self.lines.iter().rev().find_map(|line| {
56            if let Line::Entry { key_raw, value, .. } = line
57                && key_raw.eq_ignore_ascii_case(key)
58            {
59                return Some(value.as_str());
60            }
61            None
62        })
63    }
64
65    /// Get the SELinux mode.
66    #[must_use]
67    pub fn selinux(&self) -> Option<SelinuxMode> {
68        self.get(SELINUX_KEY).and_then(|v| v.parse().ok())
69    }
70
71    /// Get the SELinux policy type (raw string).
72    #[must_use]
73    pub fn selinuxtype(&self) -> Option<&str> {
74        self.get(SELINUXTYPE_KEY)
75    }
76
77    /// Get REQUIRESEUSERS as a boolean (1=true, 0=false).
78    #[must_use]
79    pub fn require_seusers(&self) -> Option<bool> {
80        self.get_bool(REQUIRESEUSERS_KEY)
81    }
82
83    /// Get AUTORELABEL as a boolean (1=true, 0=false).
84    #[must_use]
85    pub fn autorelabel(&self) -> Option<bool> {
86        self.get_bool(AUTORELABEL_KEY)
87    }
88
89    /// Get SETLOCALDEFS as a boolean (1=true, 0=false).
90    #[must_use]
91    pub fn setlocaldefs(&self) -> Option<bool> {
92        self.get_bool(SETLOCALDEFS_KEY)
93    }
94
95    /// Get a key's value interpreted as a boolean.
96    ///
97    /// `"1"` / `"true"` → `Some(true)`, `"0"` / `"false"` → `Some(false)`,
98    /// anything else → `None`.  Matching is case-insensitive.
99    #[must_use]
100    pub fn get_bool(&self, key: &str) -> Option<bool> {
101        self.get(key)
102            .and_then(|v| match v.to_ascii_lowercase().as_str() {
103                "1" | "true" => Some(true),
104                "0" | "false" => Some(false),
105                _ => None,
106            })
107    }
108
109    // -- typed setters --
110
111    /// Set the SELinux mode.
112    pub fn set_selinux(&mut self, mode: SelinuxMode) {
113        self.set_inner(SELINUX_KEY, &mode.to_string());
114    }
115
116    /// Set the SELinux policy type with validation:
117    /// - Must be non-empty after trimming
118    /// - Must not contain `/`
119    /// - Must not contain ASCII control characters
120    pub fn set_selinuxtype(&mut self, value: &str) -> Result<(), ValueError> {
121        let errors = validate_selinuxtype_value(value);
122        if let Some(e) = errors.into_iter().next() {
123            return Err(e);
124        }
125        let trimmed = value.trim();
126        self.set_inner(SELINUXTYPE_KEY, trimmed);
127        Ok(())
128    }
129
130    /// Set REQUIRESEUSERS (`"1"` / `"0"`).
131    pub fn set_require_seusers(&mut self, value: bool) {
132        self.set_inner(REQUIRESEUSERS_KEY, if value { "1" } else { "0" });
133    }
134
135    /// Set AUTORELABEL (`"1"` / `"0"`).
136    pub fn set_autorelabel(&mut self, value: bool) {
137        self.set_inner(AUTORELABEL_KEY, if value { "1" } else { "0" });
138    }
139
140    /// Set SETLOCALDEFS (`"1"` / `"0"`).
141    pub fn set_setlocaldefs(&mut self, value: bool) {
142        self.set_inner(SETLOCALDEFS_KEY, if value { "1" } else { "0" });
143    }
144
145    // -- generic API methods --
146
147    /// Generic setter.
148    ///
149    /// - Empty key → no-op
150    /// - Known keys → normalized to canonical uppercase form
151    /// - Unknown keys → caller's case is preserved
152    /// - If the key already exists → updates the **last** matching entry in-place
153    /// - If the key does not exist → appends a new `Entry` at the end
154    pub fn set(&mut self, key: &str, value: &str) {
155        if key.is_empty() {
156            return;
157        }
158        let canonical = canonical_key_name(key);
159        self.set_inner(&canonical, value);
160    }
161
162    /// Remove **all** entries matching `key` (case-insensitive).
163    ///
164    /// Returns `true` if any entries were removed.  Comments and blank lines
165    /// are not affected.
166    pub fn remove(&mut self, key: &str) -> bool {
167        let len_before = self.lines.len();
168        self.lines.retain(|line| match line {
169            Line::Entry { key_raw, .. } => !key_raw.eq_ignore_ascii_case(key),
170            _ => true,
171        });
172        self.lines.len() != len_before
173    }
174
175    /// Comment out **all** entries matching `key` (case-insensitive).
176    ///
177    /// Each matching `Entry` is converted to a `Comment` with `"# "` prepended.
178    ///
179    /// Returns `true` if any entries were disabled.
180    pub fn disable(&mut self, key: &str) -> bool {
181        let mut disabled = false;
182        for line in self.lines.iter_mut() {
183            if let Line::Entry {
184                key_raw,
185                value,
186                raw_leading,
187                raw_separator,
188                raw_suffix,
189            } = line
190                && key_raw.eq_ignore_ascii_case(key)
191            {
192                let commented = format!(
193                    "{}# {}{}{}{}",
194                    raw_leading, key_raw, raw_separator, value, raw_suffix
195                );
196                *line = Line::Comment(commented);
197                disabled = true;
198            }
199        }
200        disabled
201    }
202
203    /// Return all unique keys in order of first appearance.
204    ///
205    /// Deduplication is case-insensitive.
206    #[must_use]
207    pub fn keys(&self) -> Vec<&str> {
208        let mut seen = HashSet::new();
209        let mut result = Vec::new();
210        for line in &self.lines {
211            if let Line::Entry { key_raw, .. } = line {
212                let lower = key_raw.to_ascii_lowercase();
213                if seen.insert(lower) {
214                    result.push(key_raw.as_str());
215                }
216            }
217        }
218        result
219    }
220
221    /// Append a comment line (`# <comment>\n`).
222    pub fn add_comment_line(&mut self, comment: &str) {
223        self.lines.push(Line::Comment(format!("# {}\n", comment)));
224    }
225
226    /// Append a blank line (`\n`).
227    pub fn add_blank_line(&mut self) {
228        self.lines.push(Line::Blank(String::from("\n")));
229    }
230
231    /// Validate all entry values against known key rules.
232    ///
233    /// - `SELINUX`: must be one of `enforcing`, `permissive`, `disabled`
234    /// - `SELINUXTYPE`: must be non-empty, no `/`, no ASCII control chars
235    /// - `REQUIRESEUSERS` / `AUTORELABEL` / `SETLOCALDEFS`: 0/1/true/false
236    /// - Unknown keys: skipped
237    #[must_use]
238    pub fn validate(&self) -> Vec<ValueError> {
239        let mut errors = Vec::new();
240        for line in &self.lines {
241            if let Line::Entry { key_raw, value, .. } = line {
242                let key_upper = key_raw.to_ascii_uppercase();
243                match key_upper.as_str() {
244                    "SELINUX" if value.parse::<SelinuxMode>().is_err() => {
245                        errors.push(ValueError {
246                            key: SELINUX_KEY.into(),
247                            message: format!("invalid SELinux mode: '{}'", value),
248                        });
249                    }
250                    "SELINUXTYPE" => {
251                        errors.extend(validate_selinuxtype_value(value));
252                    }
253                    "REQUIRESEUSERS" | "AUTORELABEL" | "SETLOCALDEFS" => {
254                        if let Some(e) = validate_boolean_value(key_raw, value) {
255                            errors.push(e);
256                        }
257                    }
258                    _ => {}
259                }
260            }
261        }
262        errors
263    }
264
265    // -- internal helpers --
266
267    /// Update the last entry matching `key` (case-insensitive) with a new
268    /// value, or append a new entry if none matches.
269    #[doc(hidden)]
270    pub(crate) fn set_inner(&mut self, key: &str, value: &str) {
271        for line in self.lines.iter_mut().rev() {
272            if let Line::Entry {
273                key_raw, value: v, ..
274            } = line
275                && key_raw.eq_ignore_ascii_case(key)
276            {
277                *v = value.to_string();
278                return;
279            }
280        }
281        self.lines.push(Line::Entry {
282            key_raw: key.to_string(),
283            value: value.to_string(),
284            raw_leading: String::new(),
285            raw_separator: "=".to_string(),
286            raw_suffix: "\n".to_string(),
287        });
288    }
289}
290
291impl Default for ConfigFile {
292    fn default() -> Self {
293        ConfigFile::new()
294    }
295}
296
297impl ConfigFile {
298    /// Create a config pre-populated with the minimal valid SELinux setup:
299    /// `SELINUX=enforcing`, `SELINUXTYPE=targeted`.
300    pub fn minimal() -> Self {
301        let mut cfg = ConfigFile::new();
302        cfg.lines.push(Line::Entry {
303            key_raw: SELINUX_KEY.to_string(),
304            value: "enforcing".to_string(),
305            raw_leading: String::new(),
306            raw_separator: "=".to_string(),
307            raw_suffix: "\n".to_string(),
308        });
309        cfg.lines.push(Line::Entry {
310            key_raw: SELINUXTYPE_KEY.to_string(),
311            value: SELINUXTYPE_DEFAULT.to_string(),
312            raw_leading: String::new(),
313            raw_separator: "=".to_string(),
314            raw_suffix: "\n".to_string(),
315        });
316        cfg
317    }
318}
319
320// -- private helpers --
321
322/// Normalize known key names to canonical uppercase form.
323fn canonical_key_name(key: &str) -> String {
324    if key.eq_ignore_ascii_case(SELINUX_KEY) {
325        return SELINUX_KEY.into();
326    }
327    if key.eq_ignore_ascii_case(SELINUXTYPE_KEY) {
328        return SELINUXTYPE_KEY.into();
329    }
330    if key.eq_ignore_ascii_case(REQUIRESEUSERS_KEY) {
331        return REQUIRESEUSERS_KEY.into();
332    }
333    if key.eq_ignore_ascii_case(AUTORELABEL_KEY) {
334        return AUTORELABEL_KEY.into();
335    }
336    if key.eq_ignore_ascii_case(SETLOCALDEFS_KEY) {
337        return SETLOCALDEFS_KEY.into();
338    }
339    key.to_string()
340}
341
342/// Validate a SELINUXTYPE value: non-empty, no `/`, no ASCII control chars.
343fn validate_selinuxtype_value(value: &str) -> Vec<ValueError> {
344    let trimmed = value.trim();
345    let mut errors = Vec::new();
346    if trimmed.is_empty() {
347        errors.push(ValueError {
348            key: SELINUXTYPE_KEY.into(),
349            message: "SELINUXTYPE value must not be empty".into(),
350        });
351    }
352    if trimmed.contains('/') {
353        errors.push(ValueError {
354            key: SELINUXTYPE_KEY.into(),
355            message: format!("SELINUXTYPE value must not contain '/': '{}'", trimmed),
356        });
357    }
358    if trimmed.chars().any(|c| c.is_ascii_control()) {
359        errors.push(ValueError {
360            key: SELINUXTYPE_KEY.into(),
361            message: format!(
362                "SELINUXTYPE value contains control characters: '{}'",
363                trimmed
364            ),
365        });
366    }
367    errors
368}
369
370/// Validate a boolean config value (0/1/true/false).
371fn validate_boolean_value(key: &str, value: &str) -> Option<ValueError> {
372    let lower = value.to_ascii_lowercase();
373    if lower != "1" && lower != "0" && lower != "true" && lower != "false" {
374        Some(ValueError {
375            key: key.into(),
376            message: format!("invalid boolean value: '{}'", value),
377        })
378    } else {
379        None
380    }
381}