1use 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#[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 pub fn new() -> Self {
23 ConfigFile { lines: Vec::new() }
24 }
25
26 pub fn parse(input: &str) -> Result<Self, crate::error::ParseError> {
28 parser::parse(input)
29 }
30
31 #[must_use]
33 pub fn lines(&self) -> &[Line] {
34 &self.lines
35 }
36
37 #[must_use]
39 pub fn is_empty(&self) -> bool {
40 !self.lines.iter().any(|l| matches!(l, Line::Entry { .. }))
41 }
42
43 #[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 #[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 #[must_use]
67 pub fn selinux(&self) -> Option<SelinuxMode> {
68 self.get(SELINUX_KEY).and_then(|v| v.parse().ok())
69 }
70
71 #[must_use]
73 pub fn selinuxtype(&self) -> Option<&str> {
74 self.get(SELINUXTYPE_KEY)
75 }
76
77 #[must_use]
79 pub fn require_seusers(&self) -> Option<bool> {
80 self.get_bool(REQUIRESEUSERS_KEY)
81 }
82
83 #[must_use]
85 pub fn autorelabel(&self) -> Option<bool> {
86 self.get_bool(AUTORELABEL_KEY)
87 }
88
89 #[must_use]
91 pub fn setlocaldefs(&self) -> Option<bool> {
92 self.get_bool(SETLOCALDEFS_KEY)
93 }
94
95 #[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 pub fn set_selinux(&mut self, mode: SelinuxMode) {
113 self.set_inner(SELINUX_KEY, &mode.to_string());
114 }
115
116 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 pub fn set_require_seusers(&mut self, value: bool) {
132 self.set_inner(REQUIRESEUSERS_KEY, if value { "1" } else { "0" });
133 }
134
135 pub fn set_autorelabel(&mut self, value: bool) {
137 self.set_inner(AUTORELABEL_KEY, if value { "1" } else { "0" });
138 }
139
140 pub fn set_setlocaldefs(&mut self, value: bool) {
142 self.set_inner(SETLOCALDEFS_KEY, if value { "1" } else { "0" });
143 }
144
145 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 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 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 #[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 pub fn add_comment_line(&mut self, comment: &str) {
223 self.lines.push(Line::Comment(format!("# {}\n", comment)));
224 }
225
226 pub fn add_blank_line(&mut self) {
228 self.lines.push(Line::Blank(String::from("\n")));
229 }
230
231 #[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 #[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 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
320fn 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
342fn 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
370fn 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}