sd_switch/systemd/
ini.rs

1use std::fmt::Write as _;
2use std::{borrow::Cow, collections::BTreeMap, iter::Peekable, str::Chars};
3
4pub const KEY_REFUSEMANUALSTART: &str = "RefuseManualStart";
5pub const KEY_REFUSEMANUALSTOP: &str = "RefuseManualStop";
6pub const KEY_X_RELOADIFCHANGED: &str = "X-ReloadIfChanged";
7pub const KEY_X_RESTARTIFCHANGED: &str = "X-RestartIfChanged";
8pub const KEY_X_STOPIFCHANGED: &str = "X-StopIfChanged";
9pub const KEY_X_SWITCHMETHOD: &str = "X-SwitchMethod";
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct SystemdIni(Sections);
13
14type Sections = BTreeMap<String, Entries>;
15type Entries = BTreeMap<String, Value>;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18enum Value {
19    Strings(Vec<String>),
20    SwitchMethod(UnitSwitchMethod),
21    Bool(bool),
22}
23
24/// How to switch a unit.
25#[derive(Debug, Copy, Clone, PartialEq, Eq)]
26pub enum UnitSwitchMethod {
27    /// Reload the unit if it is already running, otherwise start it.
28    Reload,
29    /// Restart the unit if it is already running, otherwise start it.
30    Restart,
31    /// Stop the old unit (if it exists) and start the new unit.
32    StopStart,
33    /// Stop the old unit (if it exists) but do not start the new unit
34    StopOnly,
35    /// Leave the old unit running, ignoring the new unit. If no old unit
36    /// exists, then the new unit is started.
37    KeepOld,
38}
39
40impl SystemdIni {
41    pub fn get_bool(&self, section: &str, key: &str) -> Option<bool> {
42        self.get_value(section, key).and_then(|v| {
43            if let Value::Bool(b) = v {
44                Some(*b)
45            } else {
46                None
47            }
48        })
49    }
50
51    pub fn get_unit_switch_method(&self) -> Option<UnitSwitchMethod> {
52        self.get_value("Unit", KEY_X_SWITCHMETHOD).and_then(|v| {
53            if let Value::SwitchMethod(b) = v {
54                Some(*b)
55            } else {
56                None
57            }
58        })
59    }
60
61    fn get_value(&self, section: &str, key: &str) -> Option<&Value> {
62        self.0.get(section).and_then(|entries| entries.get(key))
63    }
64
65    pub fn remove(&mut self, section: &str, key: &str) {
66        let _ = self.0.get_mut(section).map(|entries| entries.remove(key));
67    }
68
69    /// Merges `other` into `self`.
70    ///
71    /// Overlapping fields are taken from `other` and `other` is consumed in the process.
72    pub fn extend(self: &mut SystemdIni, other: SystemdIni) {
73        for (section, entries) in other.0 {
74            self.0.entry(section).or_default().extend(entries);
75        }
76    }
77
78    /// Compares `self` with `other` for equality but excluding the given
79    /// options.
80    pub fn eq_excluding(&self, other: &SystemdIni, excluded: &[(&str, &str)]) -> bool {
81        /// Creates an iterator over all entries, excluding those entries present in `excluded`.
82        fn make_eq_iter<'a>(
83            ini: &'a SystemdIni,
84            excluded: &'a [(&'a str, &'a str)],
85        ) -> impl Iterator<Item = ((&'a str, &'a str), &'a Value)> {
86            ini.0
87                .iter()
88                .flat_map(|(section, entries)| {
89                    entries
90                        .iter()
91                        .map(|(key, value)| ((section.as_str(), key.as_str()), value))
92                })
93                .filter(|(p, _)| !excluded.contains(p))
94        }
95
96        let a = make_eq_iter(self, excluded);
97        let b = make_eq_iter(other, excluded);
98
99        a.eq(b)
100    }
101}
102
103struct Parser<'a> {
104    content: Peekable<Chars<'a>>,
105    line: usize,
106    column: usize,
107}
108
109#[derive(Debug, PartialEq, Eq)]
110pub struct ParseError {
111    message: Cow<'static, str>,
112    line: usize,
113    column: usize,
114}
115
116impl std::fmt::Display for ParseError {
117    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
118        write!(
119            f,
120            "{} at line {}, column {}",
121            self.message, self.line, self.column
122        )
123    }
124}
125
126impl std::error::Error for ParseError {}
127
128impl<'a> Parser<'a> {
129    fn eof(&mut self) -> bool {
130        self.content.peek().is_none()
131    }
132
133    /// Read the next character from the input stream.
134    fn next(&mut self) -> Option<char> {
135        let ch = self.content.next()?;
136
137        if ch == '\n' {
138            self.line += 1;
139            self.column = 0;
140        } else {
141            self.column += 1;
142        }
143
144        Some(ch)
145    }
146
147    /// Optionally read the next character from the input stream.
148    fn next_if<P>(&mut self, p: P) -> Option<char>
149    where
150        P: FnOnce(&char) -> bool,
151    {
152        let ch = self.content.next_if(p)?;
153
154        if ch == '\n' {
155            self.line += 1;
156            self.column = 0;
157        } else {
158            self.column += 1;
159        }
160
161        Some(ch)
162    }
163
164    fn error(&self, message: Cow<'static, str>) -> ParseError {
165        ParseError {
166            message,
167            line: self.line,
168            column: self.column,
169        }
170    }
171
172    fn error_unexpected<S: AsRef<str>>(&mut self, expected: S) -> ParseError {
173        let message = format!(
174            "expected {} but got {}",
175            expected.as_ref(),
176            if let Some(ch) = self.content.peek() {
177                format!("'{ch}'")
178            } else {
179                "end of file".to_string()
180            }
181        );
182
183        ParseError {
184            message: message.into(),
185            line: self.line,
186            column: self.column,
187        }
188    }
189
190    fn expect(&mut self, expected: char) -> Result<(), ParseError> {
191        if let Some(ch) = self.content.peek() {
192            let ch = *ch;
193            if ch == expected {
194                self.next();
195                return Ok(());
196            }
197        }
198
199        Err(self.error_unexpected(format!("'{expected}'")))
200    }
201
202    // Used by `to_unescaped_string`.
203    #[allow(dead_code)]
204    fn take(&mut self, n: usize) -> String {
205        let mut result = String::new();
206
207        for _ in 0..n {
208            if let Some(ch) = self.next() {
209                result.push(ch);
210            } else {
211                break;
212            }
213        }
214
215        result
216    }
217
218    fn take_while<P>(&mut self, p: P) -> String
219    where
220        P: Fn(&char) -> bool,
221    {
222        let mut result = String::new();
223
224        while let Some(ch) = self.next_if(&p) {
225            result.push(ch);
226        }
227
228        result
229    }
230
231    fn skip_while<P>(&mut self, p: P)
232    where
233        P: Fn(&char) -> bool,
234    {
235        while self.next_if(&p).is_some() {}
236    }
237
238    /// Skips all whitespace characters.
239    fn skip_ws(&mut self) {
240        self.skip_while(|ch| ch.is_ascii_whitespace())
241    }
242
243    /// Skip only horizontal whitespace characters (' ' and '\t').
244    fn skip_hs(&mut self) {
245        self.skip_while(|ch| *ch == ' ' || *ch == '\t')
246    }
247
248    /// Skip comment starting with `#` or `;`, if one starts at current position.
249    ///
250    /// Returns `true` if comment actually was skipped, `false` otherwise.
251    fn skip_comment(&mut self) -> bool {
252        if self.next_if(|ch| *ch == '#' || *ch == ';').is_some() {
253            self.skip_while(|ch| *ch != '\n');
254            self.next();
255            true
256        } else {
257            false
258        }
259    }
260
261    fn parse_section(&mut self) -> Result<Option<(String, Entries)>, ParseError> {
262        self.skip_ws();
263
264        while self.skip_comment() {
265            self.skip_ws();
266        }
267
268        // If we've reached EOF.
269        if self.eof() {
270            return Ok(None);
271        }
272
273        self.expect('[')?;
274
275        let section_name = self
276            .take_while(|ch| *ch != ']' && *ch != '\'' && *ch != '\"' && !ch.is_ascii_control());
277
278        self.expect(']')?;
279        self.expect('\n')?;
280        self.skip_ws();
281
282        let entries = self.parse_section_entries(&section_name)?;
283
284        Ok(Some((section_name, entries)))
285    }
286
287    fn parse_section_entries(&mut self, section_name: &str) -> Result<Entries, ParseError> {
288        let mut entries = Entries::new();
289
290        while let Some(ch) = self.content.peek() {
291            // Have reached start of a new INI section.
292            if *ch == '[' {
293                break;
294            }
295
296            if self.skip_comment() {
297                self.skip_ws();
298                continue;
299            }
300
301            let key = self.parse_entry_key()?;
302
303            if key.is_empty() {
304                return Err(self.error_unexpected("entry key name"));
305            }
306
307            self.skip_hs();
308
309            self.expect('=')?;
310
311            self.skip_hs();
312
313            let value = self.parse_entry_value()?;
314
315            // Convert the raw string value into the expected INI value.
316            match section_name {
317                "Unit" => {
318                    if [
319                        KEY_REFUSEMANUALSTART,
320                        KEY_REFUSEMANUALSTOP,
321                        KEY_X_RELOADIFCHANGED,
322                        KEY_X_RESTARTIFCHANGED,
323                        KEY_X_STOPIFCHANGED,
324                    ]
325                    .contains(&key.as_str())
326                    {
327                        let value = to_bool(value).map_err(|s| self.error(s.into()))?;
328                        entries.insert(key, value);
329                    } else if [KEY_X_SWITCHMETHOD].contains(&key.as_str()) {
330                        let value =
331                            to_unit_switch_method(&value).map_err(|s| self.error(s.into()))?;
332                        entries.insert(key, value);
333                    } else {
334                        let e = entries.entry(key).or_insert_with(|| Value::Strings(vec![]));
335                        match e {
336                            Value::Strings(items) => items.push(value),
337                            _ => panic!("inconsistent key value type"),
338                        };
339                    }
340                }
341                _ => {
342                    let e = entries.entry(key).or_insert_with(|| Value::Strings(vec![]));
343                    match e {
344                        Value::Strings(items) => items.push(value),
345                        _ => panic!("inconsistent key value type"),
346                    };
347                }
348            };
349
350            self.skip_ws();
351        }
352
353        Ok(entries)
354    }
355
356    fn parse_entry_key(&mut self) -> Result<String, ParseError> {
357        Ok(self.take_while(|c| c.is_ascii_alphanumeric() || *c == '-'))
358    }
359
360    /// Parses a entry value.
361    ///
362    /// Handles line continuations, comments, and escaped characters.
363    ///
364    /// Additionally, leading and trailing whitespaces are trimmed from the returned
365    /// string.
366    fn parse_entry_value(&mut self) -> Result<String, ParseError> {
367        let mut value = String::with_capacity(16);
368
369        while let Some(ch) = self.next() {
370            if ch == '\n' {
371                break;
372            } else if ch == '\\' {
373                let Some(ch) = self.next() else {
374                    return Err(self.error("invalid character escape: '\\'".into()));
375                };
376
377                match ch {
378                    // We are in a line continuation.
379                    '\n' => {
380                        // Skip whitespace after the \ and replace it with a
381                        // single ' '. Also skip any intermediate comment lines.
382                        value.push(' ');
383                        self.skip_hs();
384                        while self.skip_comment() {
385                            self.skip_hs();
386                        }
387                    }
388                    // Unknown escapes are passed on verbatim.
389                    ch => {
390                        value.push('\\');
391                        value.push(ch);
392                    }
393                }
394            } else {
395                value.push(ch);
396            }
397        }
398
399        Ok(value.trim().to_string())
400    }
401}
402
403/// Converts a string to a Boolean using systemd's rules.
404///
405/// As defined in `systemd.syntax(7)`:
406///
407/// > Boolean arguments used in configuration files can be written in
408/// > various formats. For positive settings the strings 1, yes, true and on
409/// > are equivalent. For negative settings, the strings 0, no, false and
410/// > off are equivalent.
411///
412/// Additionally, 'y'/'n' and 't'/'f' seem to be respected by systemd.
413fn to_bool(mut value: String) -> Result<Value, String> {
414    value.make_ascii_lowercase();
415    match value.as_str() {
416        "1" | "yes" | "y" | "true" | "t" | "on" => Ok(Value::Bool(true)),
417        "0" | "no" | "n" | "false" | "f" | "off" => Ok(Value::Bool(false)),
418        _ => Err(format!("invalid Boolean value \"{value}\"")),
419    }
420}
421
422fn to_unit_switch_method(value: &str) -> Result<Value, String> {
423    let value = match value {
424        "reload" => Ok(UnitSwitchMethod::Reload),
425        "restart" => Ok(UnitSwitchMethod::Restart),
426        "stop-start" => Ok(UnitSwitchMethod::StopStart),
427        "keep-old" => Ok(UnitSwitchMethod::KeepOld),
428        unknown => Err(format!("unknown unit switch method \"{unknown}\"")),
429    }?;
430
431    Ok(Value::SwitchMethod(value))
432}
433
434/// Unescapes the given string according to the systemd escape rules.
435///
436/// Specifically, the supported escapes match what is in
437/// `systemd.syntax(7)`:
438///
439/// | Literal    | Actual value                                        |
440/// |------------|-----------------------------------------------------|
441/// | \a         | bell                                                |
442/// | \b         | backspace                                           |
443/// | \f         | form feed                                           |
444/// | \n         | newline                                             |
445/// | \r         | carriage return                                     |
446/// | \t         | tab                                                 |
447/// | \v         | vertical tab                                        |
448/// | \\\\       | backslash                                           |
449/// | \\"        | double quotation mark                               |
450/// | \\'        | single quotation mark                               |
451/// | \s         | space                                               |
452/// | \xxx       | character number xx in hexadecimal encoding         |
453/// | \nnn       | character number nnn in octal encoding              |
454/// | \unnnn     | unicode code point nnnn in hexadecimal encoding     |
455/// | \Unnnnnnnn | unicode code point nnnnnnnn in hexadecimal encoding |
456///
457/// Note, not actually used anywhere but maybe it will be useful in the
458/// future?
459#[allow(dead_code)]
460fn to_unescaped_string(value: &str) -> Result<String, String> {
461    let mut result = String::with_capacity(value.len());
462
463    // Current character being escaped (for use of \xxx and \nnn escapes of multi-byte UTF-8 characters).
464    let mut cur_escape: Vec<u8> = Vec::with_capacity(4);
465
466    let mut it = value.chars();
467
468    while let Some(ch) = it.next() {
469        if ch == '\\' {
470            let Some(ch) = it.next() else {
471                return Err("invalid character escape: '\\'".into());
472            };
473
474            match ch {
475                'a' => result.push('\x07'),
476                'b' => result.push('\x08'),
477                'f' => result.push('\x0C'),
478                'n' => result.push('\n'),
479                'r' => result.push('\r'),
480                't' => result.push('\t'),
481                'v' => result.push('\x0B'),
482                '\\' => result.push('\\'),
483                '"' => result.push('\"'),
484                '\'' => result.push('\''),
485                's' => result.push(' '),
486                'x' => {
487                    let hex: String = it.by_ref().take(2).collect();
488                    match (hex.len(), u8::from_str_radix(&hex, 16)) {
489                        (2, Ok(char_num)) => {
490                            cur_escape.push(char_num);
491                            match std::str::from_utf8(&cur_escape) {
492                                Ok(s) => {
493                                    result.push_str(s);
494                                    cur_escape.clear();
495                                }
496                                Err(e) if e.error_len().is_none() => {}
497                                Err(_) => {
498                                    return Err(format!(
499                                        "invalid escaped UTF-8 code point '{}'",
500                                        to_hex_string(&cur_escape)
501                                    ))
502                                }
503                            }
504                        }
505                        _ => return Err(format!("invalid character escape: '\\x{hex}'")),
506                    }
507                }
508                n if ('0'..='7').contains(&n) => {
509                    let mut oct = String::with_capacity(3);
510                    oct.push(n);
511                    oct.push_str(it.by_ref().take(2).collect::<String>().as_str());
512                    let oct = oct;
513
514                    match (oct.len(), u16::from_str_radix(&oct, 8)) {
515                        (3, Ok(char_num)) if char_num <= 255 => {
516                            cur_escape.push(char_num as u8);
517                            match std::str::from_utf8(&cur_escape) {
518                                Ok(s) => {
519                                    result.push_str(s);
520                                    cur_escape.clear();
521                                }
522                                Err(e) if e.error_len().is_none() => {}
523                                Err(_) => {
524                                    return Err(format!(
525                                        "invalid escaped UTF-8 code point '{}'",
526                                        to_hex_string(&cur_escape)
527                                    ))
528                                }
529                            }
530                        }
531                        _ => return Err(format!("invalid character escape: '\\{oct}'")),
532                    }
533                }
534                'u' => {
535                    let hex: String = it.by_ref().take(4).collect();
536                    match (hex.len(), u32::from_str_radix(&hex, 16).map(char::from_u32)) {
537                        (4, Ok(Some(ch))) => result.push(ch),
538                        _ => return Err(format!("invalid character escape: '\\u{hex}'")),
539                    }
540                }
541                'U' => {
542                    let hex: String = it.by_ref().take(8).collect();
543                    match (hex.len(), u32::from_str_radix(&hex, 16).map(char::from_u32)) {
544                        (8, Ok(Some(ch))) => result.push(ch),
545                        _ => return Err(format!("invalid character escape: '\\U{hex}'")),
546                    }
547                }
548                // Unknown escapes are passed on verbatim.
549                ch => {
550                    result.push('\\');
551                    result.push(ch);
552                }
553            }
554        } else {
555            result.push(ch);
556        }
557    }
558
559    Ok(result)
560}
561
562fn to_hex_string(bytes: &[u8]) -> String {
563    bytes
564        .iter()
565        .fold(String::with_capacity(4 * bytes.len()), |mut acc, n| {
566            let _ = write!(acc, "\\x{n:02x}");
567            acc
568        })
569}
570
571/// Parses a given string as a systemd INI file.
572///
573/// The expected format is as described in [XDG Desktop Entry
574/// Specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/basic-format.html)
575/// and `systemd.syntax(7)`.
576pub fn parse(content: &str) -> Result<SystemdIni, ParseError> {
577    let mut parser = Parser {
578        content: content.chars().peekable(),
579        line: 0,
580        column: 0,
581    };
582
583    let mut sections = BTreeMap::new();
584
585    while let Some((key, entries)) = parser.parse_section()? {
586        sections.insert(key, entries);
587    }
588
589    Ok(SystemdIni(sections))
590}
591
592#[cfg(test)]
593mod tests {
594    use std::path::PathBuf;
595
596    use super::*;
597
598    fn test_data_path(file_name: &str) -> PathBuf {
599        PathBuf::from("testdata/unit_file").join(file_name)
600    }
601
602    fn systemd_ini<S: Into<Sections>>(sections: S) -> SystemdIni {
603        SystemdIni(sections.into())
604    }
605
606    fn section<M: Into<Entries>>(name: &str, entries: M) -> (String, Entries) {
607        (name.to_string(), entries.into())
608    }
609
610    fn entry(key: &str, value: Value) -> (String, Value) {
611        (key.to_string(), value)
612    }
613
614    fn entry_str(key: &str, value: &str) -> (String, Value) {
615        entry(key, Value::Strings(vec![value.to_string()]))
616    }
617
618    fn entry_bool(key: &str, value: bool) -> (String, Value) {
619        entry(key, Value::Bool(value))
620    }
621
622    #[test]
623    fn can_remove_with_empty_section() {
624        let mut actual = systemd_ini([section("Section", [])]);
625        let expected = actual.clone();
626
627        actual.remove("Missing", "missing");
628
629        assert_eq!(actual, expected);
630    }
631
632    #[test]
633    fn can_remove_with_missing_key() {
634        let mut actual = systemd_ini([section("Section", [entry_str("entry", "value")])]);
635        let expected = actual.clone();
636
637        actual.remove("Section", "missing");
638
639        assert_eq!(actual, expected);
640    }
641
642    #[test]
643    fn can_remove_with_existing_key() {
644        let mut actual = systemd_ini([section(
645            "Section",
646            [entry_str("entry", "value"), entry_str("existing", "value")],
647        )]);
648        let expected = systemd_ini([section("Section", [entry_str("entry", "value")])]);
649
650        actual.remove("Section", "existing");
651
652        assert_eq!(actual, expected);
653    }
654
655    #[test]
656    fn can_extend_empty_with_non_empty() {
657        let mut actual = systemd_ini([]);
658        let mergee = systemd_ini([section("Section", [entry_str("entry", "value")])]);
659
660        let expected = mergee.clone();
661
662        actual.extend(mergee);
663
664        assert_eq!(actual, expected);
665    }
666
667    #[test]
668    fn can_extend_non_empty_with_empty() {
669        let mut actual = systemd_ini([section("Section", [entry_str("entry", "value")])]);
670        let mergee = systemd_ini([]);
671
672        let expected = actual.clone();
673
674        actual.extend(mergee);
675
676        assert_eq!(actual, expected);
677    }
678
679    #[test]
680    fn can_extend_non_empty_section_with_empty_section() {
681        let mut actual = systemd_ini([section("Section", [entry_str("entry", "value")])]);
682        let mergee = systemd_ini([section("Section", [])]);
683
684        let expected = actual.clone();
685
686        actual.extend(mergee);
687
688        assert_eq!(actual, expected);
689    }
690
691    #[test]
692    fn can_extend_empty_section_with_non_empty_section() {
693        let mut actual = systemd_ini([section("Section", [])]);
694        let mergee = systemd_ini([section("Section", [entry_str("entry", "value")])]);
695
696        let expected = mergee.clone();
697
698        actual.extend(mergee);
699
700        assert_eq!(actual, expected);
701    }
702
703    #[test]
704    fn can_extend_non_empty_section_with_non_empty_section() {
705        let mut actual = systemd_ini([section(
706            "Section",
707            [entry_str("entry1", "value1"), entry_str("entry2", "value2")],
708        )]);
709        let mergee = systemd_ini([section(
710            "Section",
711            [entry_str("entry1", "value2"), entry_str("entry3", "value3")],
712        )]);
713
714        let expected = systemd_ini([section(
715            "Section",
716            [
717                entry_str("entry1", "value2"),
718                entry_str("entry2", "value2"),
719                entry_str("entry3", "value3"),
720            ],
721        )]);
722
723        actual.extend(mergee);
724
725        assert_eq!(actual, expected);
726    }
727
728    #[test]
729    fn can_eq_excluding_nothing() {
730        let a = systemd_ini([section(
731            "Section",
732            [entry_str("entry1", "value1"), entry_str("entry2", "value2")],
733        )]);
734
735        let same = systemd_ini([section(
736            "Section",
737            [entry_str("entry1", "value1"), entry_str("entry2", "value2")],
738        )]);
739
740        let different_value = systemd_ini([section(
741            "Section",
742            [
743                entry_str("entry1", "value1"),
744                entry_str("entry2", "value2'"),
745            ],
746        )]);
747
748        let different_entry = systemd_ini([section(
749            "Section",
750            [entry_str("entry1", "value1"), entry_str("entry3", "value3")],
751        )]);
752
753        assert!(a.eq_excluding(&same, &[]));
754        assert!(!a.eq_excluding(&different_value, &[]));
755        assert!(!a.eq_excluding(&different_entry, &[]));
756    }
757
758    #[test]
759    fn can_eq_excluding_one() {
760        let a = systemd_ini([section(
761            "Section",
762            [entry_str("entry1", "value1"), entry_str("entry2", "value2")],
763        )]);
764
765        let same = systemd_ini([section(
766            "Section",
767            [entry_str("entry1", "value1"), entry_str("entry2", "value2")],
768        )]);
769
770        let different_value = systemd_ini([section(
771            "Section",
772            [
773                entry_str("entry1", "value1"),
774                entry_str("entry2", "value2'"),
775            ],
776        )]);
777
778        let different_entry = systemd_ini([section(
779            "Section",
780            [entry_str("entry1", "value1"), entry_str("entry3", "value3")],
781        )]);
782
783        assert!(a.eq_excluding(&same, &[("Section", "entry2")]));
784        assert!(a.eq_excluding(&different_value, &[("Section", "entry2")]));
785        assert!(!a.eq_excluding(&different_entry, &[("Section", "entry2")]));
786    }
787
788    #[test]
789    fn can_parse_empty_file() {
790        let actual = parse("").unwrap();
791
792        let expected = systemd_ini([]);
793
794        assert_eq!(actual, expected);
795    }
796
797    #[test]
798    fn can_parse_file_with_only_comments() {
799        let actual = parse(
800            r#"
801# comment1
802
803; comment2
804;
805# comment3
806"#,
807        )
808        .unwrap();
809
810        let expected = systemd_ini([]);
811
812        assert_eq!(actual, expected);
813    }
814
815    #[test]
816    fn can_parse_empty_section() {
817        let actual = parse(
818            r#"
819[abcde]
820"#,
821        )
822        .unwrap();
823
824        let expected = systemd_ini([section("abcde", [])]);
825
826        assert_eq!(actual, expected);
827    }
828
829    #[test]
830    fn can_parse_file_with_comment_on_last_line() {
831        let actual = parse(
832            r#"
833[abcde]
834foo = bar
835# comment1
836; comment2"#,
837        )
838        .unwrap();
839
840        let expected = systemd_ini([section("abcde", [entry_str("foo", "bar")])]);
841
842        assert_eq!(actual, expected);
843    }
844
845    #[test]
846    fn can_parse_section() {
847        let ini_no_new_line = r#"
848[abcde]
849foo = bar
850baz = boo"#;
851        let ini_new_line = {
852            let mut s = String::from(ini_no_new_line);
853            s.push('\n');
854            s
855        };
856
857        let actual_no_new_line = parse(ini_no_new_line).unwrap();
858        let actual_new_line = parse(&ini_new_line).unwrap();
859
860        let expected = systemd_ini([section(
861            "abcde",
862            [entry_str("foo", "bar"), entry_str("baz", "boo")],
863        )]);
864
865        assert_eq!(actual_no_new_line, expected);
866        assert_eq!(actual_new_line, expected);
867    }
868
869    #[test]
870    fn fails_for_entry_without_equals() {
871        let actual = parse(
872            r#"
873[abcde]
874foo bar
875"#,
876        );
877
878        let expected = Err(ParseError {
879            message: "expected '=' but got 'b'".into(),
880            line: 2,
881            column: 4,
882        });
883
884        assert_eq!(actual, expected);
885        assert_eq!(
886            format!("{}", actual.unwrap_err()),
887            "expected '=' but got 'b' at line 2, column 4"
888        );
889    }
890
891    #[test]
892    fn fails_for_entry_without_key() {
893        let actual = parse(
894            r#"
895[abcde]
896 = bar
897"#,
898        );
899
900        let expected = Err(ParseError {
901            message: "expected entry key name but got '='".into(),
902            line: 2,
903            column: 1,
904        });
905
906        assert_eq!(actual, expected);
907        assert_eq!(
908            format!("{}", actual.unwrap_err()),
909            "expected entry key name but got '=' at line 2, column 1"
910        );
911    }
912
913    #[test]
914    fn fails_for_entry_with_bad_boolean() {
915        let actual = parse(
916            r#"
917[Unit]
918RefuseManualStart = nonsense
919"#,
920        );
921
922        let expected = Err(ParseError {
923            message: "invalid Boolean value \"nonsense\"".into(),
924            line: 3,
925            column: 0,
926        });
927
928        assert_eq!(actual, expected);
929        assert_eq!(
930            format!("{}", actual.unwrap_err()),
931            "invalid Boolean value \"nonsense\" at line 3, column 0"
932        );
933    }
934
935    #[test]
936    fn fails_for_entry_with_bad_unit_switch_method() {
937        let actual = parse(
938            r#"
939[Unit]
940X-SwitchMethod = nonsense
941"#,
942        );
943
944        let expected = Err(ParseError {
945            message: "unknown unit switch method \"nonsense\"".into(),
946            line: 3,
947            column: 0,
948        });
949
950        assert_eq!(actual, expected);
951        assert_eq!(
952            format!("{}", actual.unwrap_err()),
953            "unknown unit switch method \"nonsense\" at line 3, column 0"
954        );
955    }
956
957    #[test]
958    fn can_parse_section_with_list() {
959        let actual = parse(
960            r#"
961[abcde]
962foo = bar
963foo = baz
964"#,
965        )
966        .unwrap();
967
968        let expected = systemd_ini([section(
969            "abcde",
970            [entry(
971                "foo",
972                Value::Strings(vec!["bar".to_string(), "baz".to_string()]),
973            )],
974        )]);
975
976        assert_eq!(actual, expected);
977    }
978
979    #[test]
980    fn can_parse_section_with_line_continuation() {
981        let actual1 = parse(
982            r#"
983[abcde]
984foo = bar\
985      baz
986"#,
987        )
988        .unwrap();
989
990        let actual2 = parse(
991            r#"
992[abcde]
993foo = bar\
994    # An intervening comment should be ignored.
995    ; This comment should also be ignored.
996      baz
997"#,
998        )
999        .unwrap();
1000
1001        let expected = systemd_ini([section("abcde", [entry_str("foo", "bar baz")])]);
1002
1003        assert_eq!(actual1, expected);
1004        assert_eq!(actual2, expected);
1005    }
1006
1007    #[test]
1008    fn can_unescape_strings() {
1009        assert_eq!(to_unescaped_string("plain"), Ok("plain".to_string()));
1010
1011        // Test each escape style bracketed by '<' and '>' to avoid triggering
1012        // string trimming.
1013        assert_eq!(to_unescaped_string("<\\a>"), Ok("<\x07>".to_string()));
1014        assert_eq!(to_unescaped_string("<\\b>"), Ok("<\x08>".to_string()));
1015        assert_eq!(to_unescaped_string("<\\f>"), Ok("<\x0C>".to_string()));
1016        assert_eq!(to_unescaped_string("<\\n>"), Ok("<\n>".to_string()));
1017        assert_eq!(to_unescaped_string("<\\r>"), Ok("<\r>".to_string()));
1018        assert_eq!(to_unescaped_string("<\\t>"), Ok("<\t>".to_string()));
1019        assert_eq!(to_unescaped_string("<\\v>"), Ok("<\x0B>".to_string()));
1020        assert_eq!(to_unescaped_string("<\\\\>"), Ok("<\\>".to_string()));
1021        assert_eq!(to_unescaped_string("<\\\">"), Ok("<\">".to_string()));
1022        assert_eq!(to_unescaped_string("<\\'>"), Ok("<\'>".to_string()));
1023        assert_eq!(to_unescaped_string("<\\s>"), Ok("< >".to_string()));
1024
1025        assert_eq!(to_unescaped_string("<\\x52>"), Ok("<R>".to_string()));
1026        assert_eq!(to_unescaped_string("<\\122>"), Ok("<R>".to_string()));
1027        assert_eq!(to_unescaped_string("<\\u0052>"), Ok("<R>".to_string()));
1028        assert_eq!(to_unescaped_string("<\\U00000052>"), Ok("<R>".to_string()));
1029
1030        // Unknown escape is passed on as-is.
1031        assert_eq!(to_unescaped_string("<\\h>"), Ok("<\\h>".to_string()));
1032
1033        // Technically the below are more strict than systemd in relaxed mode but
1034        // probably good to error out for invalid escapes anyway.
1035        assert_eq!(
1036            to_unescaped_string("<\\x1>"),
1037            Err("invalid character escape: '\\x1>'".into())
1038        );
1039        assert_eq!(
1040            to_unescaped_string("<\\199>"),
1041            Err("invalid character escape: '\\199'".into())
1042        );
1043        assert_eq!(
1044            to_unescaped_string("<\\u123>"),
1045            Err("invalid character escape: '\\u123>'".into())
1046        );
1047        assert_eq!(
1048            to_unescaped_string("<\\U1234>"),
1049            Err("invalid character escape: '\\U1234>'".into())
1050        );
1051        assert_eq!(
1052            to_unescaped_string("<\\"),
1053            Err("invalid character escape: '\\'".into())
1054        );
1055        assert_eq!(
1056            to_unescaped_string("<\\xf0\\xff>"),
1057            Err("invalid escaped UTF-8 code point '\\xf0\\xff'".into())
1058        );
1059        assert_eq!(
1060            to_unescaped_string("<\\360\\377>"),
1061            Err("invalid escaped UTF-8 code point '\\xf0\\xff'".into())
1062        );
1063    }
1064
1065    #[test]
1066    fn can_parse_complicated_systemd_unit() {
1067        let content = std::fs::read_to_string(test_data_path("escaped-values.service")).unwrap();
1068        let actual = parse(&content).unwrap();
1069
1070        let expected = systemd_ini([
1071            section("Install", [entry_str("WantedBy", "default.target")]),
1072            section(
1073                "Service",
1074                [
1075                    entry_str(
1076                        "ExecStart",
1077                        "/bin/sh -c 'systemd\\x2Dnotify READY=1; /run/current\\x2dsystem/sw/bin/sleep 10s'",
1078                    ),
1079                    entry_str("NotifyAccess", "all"),
1080                    entry_str("Type", "notify"),
1081                ],
1082            ),
1083            section(
1084                "Unit",
1085                [
1086                    entry_str(
1087                        "Description",
1088                        "Successful simple service with X-RestartIfChanged = false\\x20\\xf0\\x9f\\x99\\x82",
1089                    ),
1090                    entry_bool("RefuseManualStart", false),
1091                    entry_bool("RefuseManualStop", true),
1092                    entry("X-SwitchMethod", Value::SwitchMethod(UnitSwitchMethod::Restart)),
1093                ],
1094            ),
1095        ]);
1096
1097        assert_eq!(actual, expected);
1098    }
1099
1100    #[test]
1101    fn can_parse_upstream_systemd_unit() {
1102        let content =
1103            std::fs::read_to_string(test_data_path("systemd-tmpfiles-setup.service")).unwrap();
1104        let actual = parse(&content).unwrap();
1105
1106        let expected = systemd_ini([
1107            section("Install", [entry_str("WantedBy", "basic.target")]),
1108            section(
1109                "Service",
1110                [
1111                    entry_str("Type", "oneshot"),
1112                    entry_str(
1113                        "ExecStart",
1114                        "systemd-tmpfiles --user --create --remove --boot",
1115                    ),
1116                    entry_str("RemainAfterExit", "yes"),
1117                    entry_str("SuccessExitStatus", "DATAERR"),
1118                ],
1119            ),
1120            section(
1121                "Unit",
1122                [
1123                    entry_str("Description", "Create User Files and Directories"),
1124                    entry_str("Documentation", "man:tmpfiles.d(5) man:systemd-tmpfiles(8)"),
1125                    entry_str("DefaultDependencies", "no"),
1126                    entry_str("Conflicts", "shutdown.target"),
1127                    entry_str("Before", "basic.target shutdown.target"),
1128                    entry_bool("RefuseManualStop", true),
1129                ],
1130            ),
1131        ]);
1132
1133        assert_eq!(actual, expected);
1134    }
1135}