sd_switch/systemd/
ini.rs

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