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#[derive(Debug, Copy, Clone, PartialEq, Eq)]
25pub enum UnitSwitchMethod {
26 Reload,
28 Restart,
30 StopStart,
32 StopOnly,
34 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 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 pub fn eq_excluding(&self, other: &SystemdIni, excluded: &[(&str, &str)]) -> bool {
80 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 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 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 #[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 fn skip_ws(&mut self) {
239 self.skip_while(|ch| ch.is_ascii_whitespace())
240 }
241
242 fn skip_hs(&mut self) {
244 self.skip_while(|ch| *ch == ' ' || *ch == '\t')
245 }
246
247 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 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(§ion_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 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 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 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 '\n' => {
379 value.push(' ');
382 self.skip_hs();
383 while self.skip_comment() {
384 self.skip_hs();
385 }
386 }
387 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
402fn 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#[allow(dead_code)]
459fn to_unescaped_string(value: &str) -> Result<String, String> {
460 let mut result = String::with_capacity(value.len());
461
462 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 ch => {
555 result.push('\\');
556 result.push(ch);
557 }
558 }
559 } else {
560 result.push(ch);
561 }
562 }
563
564 Ok(result)
565}
566
567pub 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 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 assert_eq!(to_unescaped_string("<\\h>"), Ok("<\\h>".to_string()));
994
995 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}