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#[derive(Debug, Copy, Clone, PartialEq, Eq)]
26pub enum UnitSwitchMethod {
27 Reload,
29 Restart,
31 StopStart,
33 StopOnly,
35 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 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 pub fn eq_excluding(&self, other: &SystemdIni, excluded: &[(&str, &str)]) -> bool {
81 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 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 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 #[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 fn skip_ws(&mut self) {
240 self.skip_while(|ch| ch.is_ascii_whitespace())
241 }
242
243 fn skip_hs(&mut self) {
245 self.skip_while(|ch| *ch == ' ' || *ch == '\t')
246 }
247
248 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 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(§ion_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 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 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 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 '\n' => {
380 value.push(' ');
383 self.skip_hs();
384 while self.skip_comment() {
385 self.skip_hs();
386 }
387 }
388 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
403fn 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#[allow(dead_code)]
460fn to_unescaped_string(value: &str) -> Result<String, String> {
461 let mut result = String::with_capacity(value.len());
462
463 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 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
571pub 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 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 assert_eq!(to_unescaped_string("<\\h>"), Ok("<\\h>".to_string()));
1032
1033 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}