1use crate::lex::{lex, SyntaxKind};
31use rowan::ast::AstNode;
32use rowan::{GreenNode, GreenNodeBuilder};
33use std::path::Path;
34use std::str::FromStr;
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct PositionedParseError {
39 pub message: String,
41 pub range: rowan::TextRange,
43 pub code: Option<String>,
45}
46
47impl std::fmt::Display for PositionedParseError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(f, "{}", self.message)
50 }
51}
52
53impl std::error::Error for PositionedParseError {}
54
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct ParseError(pub Vec<String>);
58
59impl std::fmt::Display for ParseError {
60 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
61 for err in &self.0 {
62 writeln!(f, "{}", err)?;
63 }
64 Ok(())
65 }
66}
67
68impl std::error::Error for ParseError {}
69
70#[derive(Debug)]
72pub enum Error {
73 ParseError(ParseError),
75
76 IoError(std::io::Error),
78}
79
80impl std::fmt::Display for Error {
81 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
82 match &self {
83 Error::ParseError(err) => write!(f, "{}", err),
84 Error::IoError(err) => write!(f, "{}", err),
85 }
86 }
87}
88
89impl From<ParseError> for Error {
90 fn from(err: ParseError) -> Self {
91 Self::ParseError(err)
92 }
93}
94
95impl From<std::io::Error> for Error {
96 fn from(err: std::io::Error) -> Self {
97 Self::IoError(err)
98 }
99}
100
101impl std::error::Error for Error {}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
105pub enum Lang {}
106
107impl rowan::Language for Lang {
108 type Kind = SyntaxKind;
109
110 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
111 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
112 }
113
114 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
115 kind.into()
116 }
117}
118
119pub(crate) struct ParseResult {
121 pub(crate) green_node: GreenNode,
122 pub(crate) errors: Vec<String>,
123 pub(crate) positioned_errors: Vec<PositionedParseError>,
124}
125
126pub(crate) fn parse(text: &str) -> ParseResult {
128 struct Parser<'a> {
129 tokens: Vec<(SyntaxKind, &'a str)>,
130 builder: GreenNodeBuilder<'static>,
131 errors: Vec<String>,
132 positioned_errors: Vec<PositionedParseError>,
133 pos: usize,
134 }
135
136 impl<'a> Parser<'a> {
137 fn current(&self) -> Option<SyntaxKind> {
138 if self.pos < self.tokens.len() {
139 Some(self.tokens[self.tokens.len() - 1 - self.pos].0)
140 } else {
141 None
142 }
143 }
144
145 fn bump(&mut self) {
146 if self.pos < self.tokens.len() {
147 let (kind, text) = self.tokens[self.tokens.len() - 1 - self.pos];
148 self.builder.token(kind.into(), text);
149 self.pos += 1;
150 }
151 }
152
153 fn skip_ws(&mut self) {
154 while self.current() == Some(SyntaxKind::WHITESPACE) {
155 self.bump();
156 }
157 }
158
159 fn skip_blank_lines(&mut self) {
160 while let Some(kind) = self.current() {
161 match kind {
162 SyntaxKind::NEWLINE => {
163 self.builder.start_node(SyntaxKind::BLANK_LINE.into());
164 self.bump();
165 self.builder.finish_node();
166 }
167 SyntaxKind::WHITESPACE => {
168 if self.pos + 1 < self.tokens.len()
170 && self.tokens[self.tokens.len() - 2 - self.pos].0
171 == SyntaxKind::NEWLINE
172 {
173 self.builder.start_node(SyntaxKind::BLANK_LINE.into());
174 self.bump(); self.bump(); self.builder.finish_node();
177 } else {
178 break;
179 }
180 }
181 _ => break,
182 }
183 }
184 }
185
186 fn parse_section_header(&mut self) {
187 self.builder.start_node(SyntaxKind::SECTION_HEADER.into());
188
189 if self.current() == Some(SyntaxKind::LEFT_BRACKET) {
191 self.bump();
192 } else {
193 self.errors
194 .push("expected '[' at start of section header".to_string());
195 }
196
197 if self.current() == Some(SyntaxKind::SECTION_NAME) {
199 self.bump();
200 } else {
201 self.errors
202 .push("expected section name in section header".to_string());
203 }
204
205 if self.current() == Some(SyntaxKind::RIGHT_BRACKET) {
207 self.bump();
208 } else {
209 self.errors
210 .push("expected ']' at end of section header".to_string());
211 }
212
213 if self.current() == Some(SyntaxKind::NEWLINE) {
215 self.bump();
216 }
217
218 self.builder.finish_node();
219 }
220
221 fn parse_entry(&mut self) {
222 self.builder.start_node(SyntaxKind::ENTRY.into());
223
224 if self.current() == Some(SyntaxKind::COMMENT) {
226 self.bump();
227 if self.current() == Some(SyntaxKind::NEWLINE) {
228 self.bump();
229 }
230 self.builder.finish_node();
231 return;
232 }
233
234 if self.current() == Some(SyntaxKind::KEY) {
236 self.bump();
237 } else {
238 self.errors
239 .push(format!("expected key, got {:?}", self.current()));
240 }
241
242 self.skip_ws();
243
244 if self.current() == Some(SyntaxKind::EQUALS) {
246 self.bump();
247 } else {
248 self.errors.push("expected '=' after key".to_string());
249 }
250
251 self.skip_ws();
252
253 while let Some(kind) = self.current() {
255 match kind {
256 SyntaxKind::VALUE => self.bump(),
257 SyntaxKind::LINE_CONTINUATION => {
258 self.bump();
259 self.skip_ws();
261 }
262 SyntaxKind::NEWLINE => {
263 self.bump();
264 break;
265 }
266 _ => break,
267 }
268 }
269
270 self.builder.finish_node();
271 }
272
273 fn parse_section(&mut self) {
274 self.builder.start_node(SyntaxKind::SECTION.into());
275
276 self.parse_section_header();
278
279 while let Some(kind) = self.current() {
281 match kind {
282 SyntaxKind::LEFT_BRACKET => break, SyntaxKind::KEY | SyntaxKind::COMMENT => self.parse_entry(),
284 SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE => {
285 self.skip_blank_lines();
286 }
287 _ => {
288 self.errors
289 .push(format!("unexpected token in section: {:?}", kind));
290 self.bump();
291 }
292 }
293 }
294
295 self.builder.finish_node();
296 }
297
298 fn parse_file(&mut self) {
299 self.builder.start_node(SyntaxKind::ROOT.into());
300
301 while let Some(kind) = self.current() {
303 match kind {
304 SyntaxKind::COMMENT => {
305 self.builder.start_node(SyntaxKind::ENTRY.into());
306 self.bump();
307 if self.current() == Some(SyntaxKind::NEWLINE) {
308 self.bump();
309 }
310 self.builder.finish_node();
311 }
312 SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE => {
313 self.skip_blank_lines();
314 }
315 _ => break,
316 }
317 }
318
319 while self.current().is_some() {
321 if self.current() == Some(SyntaxKind::LEFT_BRACKET) {
322 self.parse_section();
323 } else {
324 self.errors
325 .push(format!("expected section header, got {:?}", self.current()));
326 self.bump();
327 }
328 }
329
330 self.builder.finish_node();
331 }
332 }
333
334 let mut tokens: Vec<_> = lex(text).collect();
335 tokens.reverse();
336
337 let mut parser = Parser {
338 tokens,
339 builder: GreenNodeBuilder::new(),
340 errors: Vec::new(),
341 positioned_errors: Vec::new(),
342 pos: 0,
343 };
344
345 parser.parse_file();
346
347 ParseResult {
348 green_node: parser.builder.finish(),
349 errors: parser.errors,
350 positioned_errors: parser.positioned_errors,
351 }
352}
353
354type SyntaxNode = rowan::SyntaxNode<Lang>;
356
357#[derive(Debug, Clone, PartialEq, Eq, Hash)]
359pub struct SystemdUnit(SyntaxNode);
360
361impl SystemdUnit {
362 pub fn sections(&self) -> impl Iterator<Item = Section> {
364 self.0.children().filter_map(Section::cast)
365 }
366
367 pub fn get_section(&self, name: &str) -> Option<Section> {
369 self.sections().find(|s| s.name().as_deref() == Some(name))
370 }
371
372 pub fn add_section(&mut self, name: &str) {
374 let new_section = Section::new(name);
375 let insertion_index = self.0.children_with_tokens().count();
376 self.0
377 .splice_children(insertion_index..insertion_index, vec![new_section.0.into()]);
378 }
379
380 pub fn syntax(&self) -> &SyntaxNode {
382 &self.0
383 }
384
385 pub fn text(&self) -> String {
387 self.0.text().to_string()
388 }
389
390 pub fn from_file(path: &Path) -> Result<Self, Error> {
392 let text = std::fs::read_to_string(path)?;
393 Self::from_str(&text)
394 }
395
396 pub fn write_to_file(&self, path: &Path) -> Result<(), Error> {
398 std::fs::write(path, self.text())?;
399 Ok(())
400 }
401}
402
403impl AstNode for SystemdUnit {
404 type Language = Lang;
405
406 fn can_cast(kind: SyntaxKind) -> bool {
407 kind == SyntaxKind::ROOT
408 }
409
410 fn cast(node: SyntaxNode) -> Option<Self> {
411 if node.kind() == SyntaxKind::ROOT {
412 Some(SystemdUnit(node))
413 } else {
414 None
415 }
416 }
417
418 fn syntax(&self) -> &SyntaxNode {
419 &self.0
420 }
421}
422
423impl FromStr for SystemdUnit {
424 type Err = Error;
425
426 fn from_str(s: &str) -> Result<Self, Self::Err> {
427 let parsed = parse(s);
428 if !parsed.errors.is_empty() {
429 return Err(Error::ParseError(ParseError(parsed.errors)));
430 }
431 let node = SyntaxNode::new_root_mut(parsed.green_node);
432 Ok(SystemdUnit::cast(node).expect("root node should be SystemdUnit"))
433 }
434}
435
436impl std::fmt::Display for SystemdUnit {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 write!(f, "{}", self.0.text())
439 }
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Hash)]
444pub struct Section(SyntaxNode);
445
446impl Section {
447 pub fn new(name: &str) -> Section {
449 use rowan::GreenNodeBuilder;
450
451 let mut builder = GreenNodeBuilder::new();
452 builder.start_node(SyntaxKind::SECTION.into());
453
454 builder.start_node(SyntaxKind::SECTION_HEADER.into());
456 builder.token(SyntaxKind::LEFT_BRACKET.into(), "[");
457 builder.token(SyntaxKind::SECTION_NAME.into(), name);
458 builder.token(SyntaxKind::RIGHT_BRACKET.into(), "]");
459 builder.token(SyntaxKind::NEWLINE.into(), "\n");
460 builder.finish_node();
461
462 builder.finish_node();
463 Section(SyntaxNode::new_root_mut(builder.finish()))
464 }
465
466 pub fn name(&self) -> Option<String> {
468 let header = self
469 .0
470 .children()
471 .find(|n| n.kind() == SyntaxKind::SECTION_HEADER)?;
472 let value = header
473 .children_with_tokens()
474 .find(|e| e.kind() == SyntaxKind::SECTION_NAME)?;
475 Some(value.as_token()?.text().to_string())
476 }
477
478 pub fn entries(&self) -> impl Iterator<Item = Entry> {
480 self.0.children().filter_map(Entry::cast)
481 }
482
483 pub fn get(&self, key: &str) -> Option<String> {
485 self.entries()
486 .find(|e| e.key().as_deref() == Some(key))
487 .and_then(|e| e.value())
488 }
489
490 pub fn get_all(&self, key: &str) -> Vec<String> {
492 self.entries()
493 .filter(|e| e.key().as_deref() == Some(key))
494 .filter_map(|e| e.value())
495 .collect()
496 }
497
498 pub fn set(&mut self, key: &str, value: &str) {
500 let new_entry = Entry::new(key, value);
501
502 for entry in self.entries() {
504 if entry.key().as_deref() == Some(key) {
505 self.0.splice_children(
506 entry.0.index()..entry.0.index() + 1,
507 vec![new_entry.0.into()],
508 );
509 return;
510 }
511 }
512
513 let insertion_index = self.0.children_with_tokens().count();
515 self.0
516 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
517 }
518
519 pub fn add(&mut self, key: &str, value: &str) {
521 let new_entry = Entry::new(key, value);
522 let insertion_index = self.0.children_with_tokens().count();
523 self.0
524 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
525 }
526
527 pub fn set_list(&mut self, key: &str, values: &[&str]) {
543 let value = values.join(" ");
544 self.set(key, &value);
545 }
546
547 pub fn get_list(&self, key: &str) -> Vec<String> {
552 self.entries()
553 .find(|e| e.key().as_deref() == Some(key))
554 .map(|e| e.value_as_list())
555 .unwrap_or_default()
556 }
557
558 pub fn get_bool(&self, key: &str) -> Option<bool> {
572 self.entries()
573 .find(|e| e.key().as_deref() == Some(key))
574 .and_then(|e| e.value_as_bool())
575 }
576
577 pub fn set_bool(&mut self, key: &str, value: bool) {
592 self.set(key, Entry::format_bool(value));
593 }
594
595 pub fn remove(&mut self, key: &str) {
597 let entry_to_remove = self.0.children().find_map(|child| {
599 let entry = Entry::cast(child)?;
600 if entry.key().as_deref() == Some(key) {
601 Some(entry)
602 } else {
603 None
604 }
605 });
606
607 if let Some(entry) = entry_to_remove {
608 entry.syntax().detach();
609 }
610 }
611
612 pub fn remove_all(&mut self, key: &str) {
614 let entries_to_remove: Vec<_> = self
616 .0
617 .children()
618 .filter_map(Entry::cast)
619 .filter(|e| e.key().as_deref() == Some(key))
620 .collect();
621
622 for entry in entries_to_remove {
623 entry.syntax().detach();
624 }
625 }
626
627 pub fn syntax(&self) -> &SyntaxNode {
629 &self.0
630 }
631}
632
633impl AstNode for Section {
634 type Language = Lang;
635
636 fn can_cast(kind: SyntaxKind) -> bool {
637 kind == SyntaxKind::SECTION
638 }
639
640 fn cast(node: SyntaxNode) -> Option<Self> {
641 if node.kind() == SyntaxKind::SECTION {
642 Some(Section(node))
643 } else {
644 None
645 }
646 }
647
648 fn syntax(&self) -> &SyntaxNode {
649 &self.0
650 }
651}
652
653fn unescape_string(s: &str) -> String {
655 let mut result = String::new();
656 let mut chars = s.chars().peekable();
657
658 while let Some(ch) = chars.next() {
659 if ch == '\\' {
660 match chars.next() {
661 Some('n') => result.push('\n'),
662 Some('t') => result.push('\t'),
663 Some('r') => result.push('\r'),
664 Some('\\') => result.push('\\'),
665 Some('"') => result.push('"'),
666 Some('\'') => result.push('\''),
667 Some('x') => {
668 let hex: String = chars.by_ref().take(2).collect();
670 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
671 result.push(byte as char);
672 } else {
673 result.push('\\');
675 result.push('x');
676 result.push_str(&hex);
677 }
678 }
679 Some('u') => {
680 let hex: String = chars.by_ref().take(4).collect();
682 if let Ok(code) = u32::from_str_radix(&hex, 16) {
683 if let Some(unicode_char) = char::from_u32(code) {
684 result.push(unicode_char);
685 } else {
686 result.push('\\');
688 result.push('u');
689 result.push_str(&hex);
690 }
691 } else {
692 result.push('\\');
694 result.push('u');
695 result.push_str(&hex);
696 }
697 }
698 Some('U') => {
699 let hex: String = chars.by_ref().take(8).collect();
701 if let Ok(code) = u32::from_str_radix(&hex, 16) {
702 if let Some(unicode_char) = char::from_u32(code) {
703 result.push(unicode_char);
704 } else {
705 result.push('\\');
707 result.push('U');
708 result.push_str(&hex);
709 }
710 } else {
711 result.push('\\');
713 result.push('U');
714 result.push_str(&hex);
715 }
716 }
717 Some(c) if c.is_ascii_digit() => {
718 let mut octal = String::from(c);
720 for _ in 0..2 {
721 if let Some(&next_ch) = chars.peek() {
722 if next_ch.is_ascii_digit() && next_ch < '8' {
723 octal.push(chars.next().unwrap());
724 } else {
725 break;
726 }
727 }
728 }
729 if let Ok(byte) = u8::from_str_radix(&octal, 8) {
730 result.push(byte as char);
731 } else {
732 result.push('\\');
734 result.push_str(&octal);
735 }
736 }
737 Some(c) => {
738 result.push('\\');
740 result.push(c);
741 }
742 None => {
743 result.push('\\');
745 }
746 }
747 } else {
748 result.push(ch);
749 }
750 }
751
752 result
753}
754
755fn escape_string(s: &str) -> String {
757 let mut result = String::new();
758
759 for ch in s.chars() {
760 match ch {
761 '\\' => result.push_str("\\\\"),
762 '\n' => result.push_str("\\n"),
763 '\t' => result.push_str("\\t"),
764 '\r' => result.push_str("\\r"),
765 '"' => result.push_str("\\\""),
766 _ => result.push(ch),
767 }
768 }
769
770 result
771}
772
773fn unquote_string(s: &str) -> String {
781 let trimmed = s.trim();
782
783 if trimmed.len() < 2 {
784 return trimmed.to_string();
785 }
786
787 let first = trimmed.chars().next();
788 let last = trimmed.chars().last();
789
790 if let (Some('"'), Some('"')) = (first, last) {
792 trimmed[1..trimmed.len() - 1].to_string()
794 } else if let (Some('\''), Some('\'')) = (first, last) {
795 trimmed[1..trimmed.len() - 1].to_string()
797 } else {
798 trimmed.to_string()
800 }
801}
802
803#[derive(Debug, Clone, PartialEq, Eq, Hash)]
805pub struct Entry(SyntaxNode);
806
807impl Entry {
808 pub fn new(key: &str, value: &str) -> Entry {
810 use rowan::GreenNodeBuilder;
811
812 let mut builder = GreenNodeBuilder::new();
813 builder.start_node(SyntaxKind::ENTRY.into());
814 builder.token(SyntaxKind::KEY.into(), key);
815 builder.token(SyntaxKind::EQUALS.into(), "=");
816 builder.token(SyntaxKind::VALUE.into(), value);
817 builder.token(SyntaxKind::NEWLINE.into(), "\n");
818 builder.finish_node();
819 Entry(SyntaxNode::new_root_mut(builder.finish()))
820 }
821
822 pub fn key(&self) -> Option<String> {
824 let key_token = self
825 .0
826 .children_with_tokens()
827 .find(|e| e.kind() == SyntaxKind::KEY)?;
828 Some(key_token.as_token()?.text().to_string())
829 }
830
831 pub fn value(&self) -> Option<String> {
833 let mut found_equals = false;
835 let mut value_parts = Vec::new();
836
837 for element in self.0.children_with_tokens() {
838 match element.kind() {
839 SyntaxKind::EQUALS => found_equals = true,
840 SyntaxKind::VALUE if found_equals => {
841 value_parts.push(element.as_token()?.text().to_string());
842 }
843 SyntaxKind::LINE_CONTINUATION if found_equals => {
844 let should_add_space = value_parts
847 .last()
848 .map(|s| !s.ends_with(' ') && !s.ends_with('\t'))
849 .unwrap_or(true);
850 if should_add_space {
851 value_parts.push(" ".to_string());
852 }
853 }
854 SyntaxKind::WHITESPACE if found_equals && !value_parts.is_empty() => {
855 value_parts.push(element.as_token()?.text().to_string());
858 }
859 SyntaxKind::NEWLINE => break,
860 _ => {}
861 }
862 }
863
864 if value_parts.is_empty() {
865 None
866 } else {
867 Some(value_parts.join(""))
869 }
870 }
871
872 pub fn raw_value(&self) -> Option<String> {
874 let mut found_equals = false;
875 let mut value_parts = Vec::new();
876
877 for element in self.0.children_with_tokens() {
878 match element.kind() {
879 SyntaxKind::EQUALS => found_equals = true,
880 SyntaxKind::VALUE if found_equals => {
881 value_parts.push(element.as_token()?.text().to_string());
882 }
883 SyntaxKind::LINE_CONTINUATION if found_equals => {
884 value_parts.push(element.as_token()?.text().to_string());
885 }
886 SyntaxKind::WHITESPACE if found_equals => {
887 value_parts.push(element.as_token()?.text().to_string());
888 }
889 SyntaxKind::NEWLINE => break,
890 _ => {}
891 }
892 }
893
894 if value_parts.is_empty() {
895 None
896 } else {
897 Some(value_parts.join(""))
898 }
899 }
900
901 pub fn unescape_value(&self) -> Option<String> {
915 let value = self.value()?;
916 Some(unescape_string(&value))
917 }
918
919 pub fn escape_value(value: &str) -> String {
928 escape_string(value)
929 }
930
931 pub fn is_quoted(&self) -> Option<char> {
936 let value = self.value()?;
937 let trimmed = value.trim();
938
939 if trimmed.len() < 2 {
940 return None;
941 }
942
943 let first = trimmed.chars().next()?;
944 let last = trimmed.chars().last()?;
945
946 if (first == '"' || first == '\'') && first == last {
947 Some(first)
948 } else {
949 None
950 }
951 }
952
953 pub fn unquoted_value(&self) -> Option<String> {
958 let value = self.value()?;
959 Some(unquote_string(&value))
960 }
961
962 pub fn quoted_value(&self) -> Option<String> {
966 self.value()
968 }
969
970 pub fn value_as_list(&self) -> Vec<String> {
978 let value = match self.unquoted_value() {
979 Some(v) => v,
980 None => return Vec::new(),
981 };
982
983 value.split_whitespace().map(|s| s.to_string()).collect()
984 }
985
986 pub fn value_as_bool(&self) -> Option<bool> {
1005 let value = self.unquoted_value()?;
1006 let value_lower = value.trim().to_lowercase();
1007
1008 match value_lower.as_str() {
1009 "1" | "yes" | "true" | "on" => Some(true),
1010 "0" | "no" | "false" | "off" => Some(false),
1011 _ => None,
1012 }
1013 }
1014
1015 pub fn format_bool(value: bool) -> &'static str {
1029 if value {
1030 "yes"
1031 } else {
1032 "no"
1033 }
1034 }
1035
1036 pub fn expand_specifiers(
1056 &self,
1057 context: &crate::specifier::SpecifierContext,
1058 ) -> Option<String> {
1059 let value = self.value()?;
1060 Some(context.expand(&value))
1061 }
1062
1063 pub fn syntax(&self) -> &SyntaxNode {
1065 &self.0
1066 }
1067}
1068
1069impl AstNode for Entry {
1070 type Language = Lang;
1071
1072 fn can_cast(kind: SyntaxKind) -> bool {
1073 kind == SyntaxKind::ENTRY
1074 }
1075
1076 fn cast(node: SyntaxNode) -> Option<Self> {
1077 if node.kind() == SyntaxKind::ENTRY {
1078 Some(Entry(node))
1079 } else {
1080 None
1081 }
1082 }
1083
1084 fn syntax(&self) -> &SyntaxNode {
1085 &self.0
1086 }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092
1093 #[test]
1094 fn test_parse_simple() {
1095 let input = r#"[Unit]
1096Description=Test Service
1097After=network.target
1098"#;
1099 let unit = SystemdUnit::from_str(input).unwrap();
1100 assert_eq!(unit.sections().count(), 1);
1101
1102 let section = unit.sections().next().unwrap();
1103 assert_eq!(section.name(), Some("Unit".to_string()));
1104 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1105 assert_eq!(section.get("After"), Some("network.target".to_string()));
1106 }
1107
1108 #[test]
1109 fn test_parse_with_comments() {
1110 let input = r#"# Top comment
1111[Unit]
1112# Comment before description
1113Description=Test Service
1114; Semicolon comment
1115After=network.target
1116"#;
1117 let unit = SystemdUnit::from_str(input).unwrap();
1118 assert_eq!(unit.sections().count(), 1);
1119
1120 let section = unit.sections().next().unwrap();
1121 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1122 }
1123
1124 #[test]
1125 fn test_parse_multiple_sections() {
1126 let input = r#"[Unit]
1127Description=Test Service
1128
1129[Service]
1130Type=simple
1131ExecStart=/usr/bin/test
1132
1133[Install]
1134WantedBy=multi-user.target
1135"#;
1136 let unit = SystemdUnit::from_str(input).unwrap();
1137 assert_eq!(unit.sections().count(), 3);
1138
1139 let unit_section = unit.get_section("Unit").unwrap();
1140 assert_eq!(
1141 unit_section.get("Description"),
1142 Some("Test Service".to_string())
1143 );
1144
1145 let service_section = unit.get_section("Service").unwrap();
1146 assert_eq!(service_section.get("Type"), Some("simple".to_string()));
1147 assert_eq!(
1148 service_section.get("ExecStart"),
1149 Some("/usr/bin/test".to_string())
1150 );
1151
1152 let install_section = unit.get_section("Install").unwrap();
1153 assert_eq!(
1154 install_section.get("WantedBy"),
1155 Some("multi-user.target".to_string())
1156 );
1157 }
1158
1159 #[test]
1160 fn test_parse_with_spaces() {
1161 let input = "[Unit]\nDescription = Test Service\n";
1162 let unit = SystemdUnit::from_str(input).unwrap();
1163
1164 let section = unit.sections().next().unwrap();
1165 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1166 }
1167
1168 #[test]
1169 fn test_line_continuation() {
1170 let input = "[Service]\nExecStart=/bin/echo \\\n hello world\n";
1171 let unit = SystemdUnit::from_str(input).unwrap();
1172
1173 let section = unit.sections().next().unwrap();
1174 let entry = section.entries().next().unwrap();
1175 assert_eq!(entry.key(), Some("ExecStart".to_string()));
1176 assert_eq!(entry.value(), Some("/bin/echo hello world".to_string()));
1178 }
1179
1180 #[test]
1181 fn test_lossless_roundtrip() {
1182 let input = r#"# Comment
1183[Unit]
1184Description=Test Service
1185After=network.target
1186
1187[Service]
1188Type=simple
1189ExecStart=/usr/bin/test
1190"#;
1191 let unit = SystemdUnit::from_str(input).unwrap();
1192 let output = unit.text();
1193 assert_eq!(input, output);
1194 }
1195
1196 #[test]
1197 fn test_set_value() {
1198 let input = r#"[Unit]
1199Description=Test Service
1200"#;
1201 let unit = SystemdUnit::from_str(input).unwrap();
1202 {
1203 let mut section = unit.sections().next().unwrap();
1204 section.set("Description", "Updated Service");
1205 }
1206
1207 let section = unit.sections().next().unwrap();
1208 assert_eq!(
1209 section.get("Description"),
1210 Some("Updated Service".to_string())
1211 );
1212 }
1213
1214 #[test]
1215 fn test_add_new_entry() {
1216 let input = r#"[Unit]
1217Description=Test Service
1218"#;
1219 let unit = SystemdUnit::from_str(input).unwrap();
1220 {
1221 let mut section = unit.sections().next().unwrap();
1222 section.set("After", "network.target");
1223 }
1224
1225 let section = unit.sections().next().unwrap();
1226 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1227 assert_eq!(section.get("After"), Some("network.target".to_string()));
1228 }
1229
1230 #[test]
1231 fn test_multiple_values_same_key() {
1232 let input = r#"[Unit]
1233Wants=foo.service
1234Wants=bar.service
1235"#;
1236 let unit = SystemdUnit::from_str(input).unwrap();
1237 let section = unit.sections().next().unwrap();
1238
1239 assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
1241
1242 let all_wants = section.get_all("Wants");
1244 assert_eq!(all_wants.len(), 2);
1245 assert_eq!(all_wants[0], "foo.service");
1246 assert_eq!(all_wants[1], "bar.service");
1247 }
1248
1249 #[test]
1250 fn test_add_multiple_entries() {
1251 let input = r#"[Unit]
1252Description=Test Service
1253"#;
1254 let unit = SystemdUnit::from_str(input).unwrap();
1255 {
1256 let mut section = unit.sections().next().unwrap();
1257 section.add("Wants", "foo.service");
1258 section.add("Wants", "bar.service");
1259 }
1260
1261 let section = unit.sections().next().unwrap();
1262 let all_wants = section.get_all("Wants");
1263 assert_eq!(all_wants.len(), 2);
1264 assert_eq!(all_wants[0], "foo.service");
1265 assert_eq!(all_wants[1], "bar.service");
1266 }
1267
1268 #[test]
1269 fn test_remove_entry() {
1270 let input = r#"[Unit]
1271Description=Test Service
1272After=network.target
1273"#;
1274 let unit = SystemdUnit::from_str(input).unwrap();
1275 {
1276 let mut section = unit.sections().next().unwrap();
1277 section.remove("After");
1278 }
1279
1280 let section = unit.sections().next().unwrap();
1281 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1282 assert_eq!(section.get("After"), None);
1283 }
1284
1285 #[test]
1286 fn test_remove_all_entries() {
1287 let input = r#"[Unit]
1288Wants=foo.service
1289Wants=bar.service
1290Description=Test
1291"#;
1292 let unit = SystemdUnit::from_str(input).unwrap();
1293 {
1294 let mut section = unit.sections().next().unwrap();
1295 section.remove_all("Wants");
1296 }
1297
1298 let section = unit.sections().next().unwrap();
1299 assert_eq!(section.get_all("Wants").len(), 0);
1300 assert_eq!(section.get("Description"), Some("Test".to_string()));
1301 }
1302
1303 #[test]
1304 fn test_unescape_basic() {
1305 let input = r#"[Unit]
1306Description=Test\nService
1307"#;
1308 let unit = SystemdUnit::from_str(input).unwrap();
1309 let section = unit.sections().next().unwrap();
1310 let entry = section.entries().next().unwrap();
1311
1312 assert_eq!(entry.value(), Some("Test\\nService".to_string()));
1313 assert_eq!(entry.unescape_value(), Some("Test\nService".to_string()));
1314 }
1315
1316 #[test]
1317 fn test_unescape_all_escapes() {
1318 let input = r#"[Unit]
1319Value=\n\t\r\\\"\'\x41\101\u0041\U00000041
1320"#;
1321 let unit = SystemdUnit::from_str(input).unwrap();
1322 let section = unit.sections().next().unwrap();
1323 let entry = section.entries().next().unwrap();
1324
1325 let unescaped = entry.unescape_value().unwrap();
1326 assert_eq!(unescaped, "\n\t\r\\\"'AAAA");
1330 }
1331
1332 #[test]
1333 fn test_unescape_unicode() {
1334 let input = r#"[Unit]
1335Value=Hello\u0020World\U0001F44D
1336"#;
1337 let unit = SystemdUnit::from_str(input).unwrap();
1338 let section = unit.sections().next().unwrap();
1339 let entry = section.entries().next().unwrap();
1340
1341 let unescaped = entry.unescape_value().unwrap();
1342 assert_eq!(unescaped, "Hello World๐");
1344 }
1345
1346 #[test]
1347 fn test_escape_value() {
1348 let text = "Hello\nWorld\t\"Test\"\\Path";
1349 let escaped = Entry::escape_value(text);
1350 assert_eq!(escaped, "Hello\\nWorld\\t\\\"Test\\\"\\\\Path");
1351 }
1352
1353 #[test]
1354 fn test_escape_unescape_roundtrip() {
1355 let original = "Test\nwith\ttabs\rand\"quotes\"\\backslash";
1356 let escaped = Entry::escape_value(original);
1357 let unescaped = unescape_string(&escaped);
1358 assert_eq!(original, unescaped);
1359 }
1360
1361 #[test]
1362 fn test_unescape_invalid_sequences() {
1363 let input = r#"[Unit]
1365Value=\z\xFF\u12\U1234
1366"#;
1367 let unit = SystemdUnit::from_str(input).unwrap();
1368 let section = unit.sections().next().unwrap();
1369 let entry = section.entries().next().unwrap();
1370
1371 let unescaped = entry.unescape_value().unwrap();
1372 assert!(unescaped.contains("\\z"));
1374 }
1375
1376 #[test]
1377 fn test_quoted_double_quotes() {
1378 let input = r#"[Unit]
1379Description="Test Service"
1380"#;
1381 let unit = SystemdUnit::from_str(input).unwrap();
1382 let section = unit.sections().next().unwrap();
1383 let entry = section.entries().next().unwrap();
1384
1385 assert_eq!(entry.value(), Some("\"Test Service\"".to_string()));
1386 assert_eq!(entry.quoted_value(), Some("\"Test Service\"".to_string()));
1387 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1388 assert_eq!(entry.is_quoted(), Some('"'));
1389 }
1390
1391 #[test]
1392 fn test_quoted_single_quotes() {
1393 let input = r#"[Unit]
1394Description='Test Service'
1395"#;
1396 let unit = SystemdUnit::from_str(input).unwrap();
1397 let section = unit.sections().next().unwrap();
1398 let entry = section.entries().next().unwrap();
1399
1400 assert_eq!(entry.value(), Some("'Test Service'".to_string()));
1401 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1402 assert_eq!(entry.is_quoted(), Some('\''));
1403 }
1404
1405 #[test]
1406 fn test_quoted_with_whitespace() {
1407 let input = r#"[Unit]
1408Description=" Test Service "
1409"#;
1410 let unit = SystemdUnit::from_str(input).unwrap();
1411 let section = unit.sections().next().unwrap();
1412 let entry = section.entries().next().unwrap();
1413
1414 assert_eq!(entry.unquoted_value(), Some(" Test Service ".to_string()));
1416 }
1417
1418 #[test]
1419 fn test_unquoted_value() {
1420 let input = r#"[Unit]
1421Description=Test Service
1422"#;
1423 let unit = SystemdUnit::from_str(input).unwrap();
1424 let section = unit.sections().next().unwrap();
1425 let entry = section.entries().next().unwrap();
1426
1427 assert_eq!(entry.value(), Some("Test Service".to_string()));
1428 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1429 assert_eq!(entry.is_quoted(), None);
1430 }
1431
1432 #[test]
1433 fn test_mismatched_quotes() {
1434 let input = r#"[Unit]
1435Description="Test Service'
1436"#;
1437 let unit = SystemdUnit::from_str(input).unwrap();
1438 let section = unit.sections().next().unwrap();
1439 let entry = section.entries().next().unwrap();
1440
1441 assert_eq!(entry.is_quoted(), None);
1443 assert_eq!(entry.unquoted_value(), Some("\"Test Service'".to_string()));
1444 }
1445
1446 #[test]
1447 fn test_empty_quotes() {
1448 let input = r#"[Unit]
1449Description=""
1450"#;
1451 let unit = SystemdUnit::from_str(input).unwrap();
1452 let section = unit.sections().next().unwrap();
1453 let entry = section.entries().next().unwrap();
1454
1455 assert_eq!(entry.is_quoted(), Some('"'));
1456 assert_eq!(entry.unquoted_value(), Some("".to_string()));
1457 }
1458
1459 #[test]
1460 fn test_value_as_list() {
1461 let input = r#"[Unit]
1462After=network.target remote-fs.target
1463"#;
1464 let unit = SystemdUnit::from_str(input).unwrap();
1465 let section = unit.sections().next().unwrap();
1466 let entry = section.entries().next().unwrap();
1467
1468 let list = entry.value_as_list();
1469 assert_eq!(list.len(), 2);
1470 assert_eq!(list[0], "network.target");
1471 assert_eq!(list[1], "remote-fs.target");
1472 }
1473
1474 #[test]
1475 fn test_value_as_list_single() {
1476 let input = r#"[Unit]
1477After=network.target
1478"#;
1479 let unit = SystemdUnit::from_str(input).unwrap();
1480 let section = unit.sections().next().unwrap();
1481 let entry = section.entries().next().unwrap();
1482
1483 let list = entry.value_as_list();
1484 assert_eq!(list.len(), 1);
1485 assert_eq!(list[0], "network.target");
1486 }
1487
1488 #[test]
1489 fn test_value_as_list_empty() {
1490 let input = r#"[Unit]
1491After=
1492"#;
1493 let unit = SystemdUnit::from_str(input).unwrap();
1494 let section = unit.sections().next().unwrap();
1495 let entry = section.entries().next().unwrap();
1496
1497 let list = entry.value_as_list();
1498 assert_eq!(list.len(), 0);
1499 }
1500
1501 #[test]
1502 fn test_value_as_list_with_extra_whitespace() {
1503 let input = r#"[Unit]
1504After= network.target remote-fs.target
1505"#;
1506 let unit = SystemdUnit::from_str(input).unwrap();
1507 let section = unit.sections().next().unwrap();
1508 let entry = section.entries().next().unwrap();
1509
1510 let list = entry.value_as_list();
1511 assert_eq!(list.len(), 2);
1512 assert_eq!(list[0], "network.target");
1513 assert_eq!(list[1], "remote-fs.target");
1514 }
1515
1516 #[test]
1517 fn test_section_get_list() {
1518 let input = r#"[Unit]
1519After=network.target remote-fs.target
1520"#;
1521 let unit = SystemdUnit::from_str(input).unwrap();
1522 let section = unit.sections().next().unwrap();
1523
1524 let list = section.get_list("After");
1525 assert_eq!(list.len(), 2);
1526 assert_eq!(list[0], "network.target");
1527 assert_eq!(list[1], "remote-fs.target");
1528 }
1529
1530 #[test]
1531 fn test_section_get_list_missing() {
1532 let input = r#"[Unit]
1533Description=Test
1534"#;
1535 let unit = SystemdUnit::from_str(input).unwrap();
1536 let section = unit.sections().next().unwrap();
1537
1538 let list = section.get_list("After");
1539 assert_eq!(list.len(), 0);
1540 }
1541
1542 #[test]
1543 fn test_section_set_list() {
1544 let input = r#"[Unit]
1545Description=Test
1546"#;
1547 let unit = SystemdUnit::from_str(input).unwrap();
1548 {
1549 let mut section = unit.sections().next().unwrap();
1550 section.set_list("After", &["network.target", "remote-fs.target"]);
1551 }
1552
1553 let section = unit.sections().next().unwrap();
1554 let list = section.get_list("After");
1555 assert_eq!(list.len(), 2);
1556 assert_eq!(list[0], "network.target");
1557 assert_eq!(list[1], "remote-fs.target");
1558 }
1559
1560 #[test]
1561 fn test_section_set_list_replaces() {
1562 let input = r#"[Unit]
1563After=foo.target
1564"#;
1565 let unit = SystemdUnit::from_str(input).unwrap();
1566 {
1567 let mut section = unit.sections().next().unwrap();
1568 section.set_list("After", &["network.target", "remote-fs.target"]);
1569 }
1570
1571 let section = unit.sections().next().unwrap();
1572 let list = section.get_list("After");
1573 assert_eq!(list.len(), 2);
1574 assert_eq!(list[0], "network.target");
1575 assert_eq!(list[1], "remote-fs.target");
1576 }
1577
1578 #[test]
1579 fn test_value_as_bool_positive() {
1580 let inputs = vec!["yes", "true", "1", "on", "YES", "True", "ON"];
1581
1582 for input_val in inputs {
1583 let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1584 let unit = SystemdUnit::from_str(&input).unwrap();
1585 let section = unit.sections().next().unwrap();
1586 let entry = section.entries().next().unwrap();
1587 assert_eq!(
1588 entry.value_as_bool(),
1589 Some(true),
1590 "Failed for input: {}",
1591 input_val
1592 );
1593 }
1594 }
1595
1596 #[test]
1597 fn test_value_as_bool_negative() {
1598 let inputs = vec!["no", "false", "0", "off", "NO", "False", "OFF"];
1599
1600 for input_val in inputs {
1601 let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1602 let unit = SystemdUnit::from_str(&input).unwrap();
1603 let section = unit.sections().next().unwrap();
1604 let entry = section.entries().next().unwrap();
1605 assert_eq!(
1606 entry.value_as_bool(),
1607 Some(false),
1608 "Failed for input: {}",
1609 input_val
1610 );
1611 }
1612 }
1613
1614 #[test]
1615 fn test_value_as_bool_invalid() {
1616 let input = r#"[Service]
1617RemainAfterExit=maybe
1618"#;
1619 let unit = SystemdUnit::from_str(input).unwrap();
1620 let section = unit.sections().next().unwrap();
1621 let entry = section.entries().next().unwrap();
1622 assert_eq!(entry.value_as_bool(), None);
1623 }
1624
1625 #[test]
1626 fn test_value_as_bool_with_whitespace() {
1627 let input = r#"[Service]
1628RemainAfterExit= yes
1629"#;
1630 let unit = SystemdUnit::from_str(input).unwrap();
1631 let section = unit.sections().next().unwrap();
1632 let entry = section.entries().next().unwrap();
1633 assert_eq!(entry.value_as_bool(), Some(true));
1634 }
1635
1636 #[test]
1637 fn test_format_bool() {
1638 assert_eq!(Entry::format_bool(true), "yes");
1639 assert_eq!(Entry::format_bool(false), "no");
1640 }
1641
1642 #[test]
1643 fn test_section_get_bool() {
1644 let input = r#"[Service]
1645RemainAfterExit=yes
1646Type=simple
1647"#;
1648 let unit = SystemdUnit::from_str(input).unwrap();
1649 let section = unit.sections().next().unwrap();
1650
1651 assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1652 assert_eq!(section.get_bool("Type"), None); assert_eq!(section.get_bool("Missing"), None); }
1655
1656 #[test]
1657 fn test_section_set_bool() {
1658 let input = r#"[Service]
1659Type=simple
1660"#;
1661 let unit = SystemdUnit::from_str(input).unwrap();
1662 {
1663 let mut section = unit.sections().next().unwrap();
1664 section.set_bool("RemainAfterExit", true);
1665 section.set_bool("PrivateTmp", false);
1666 }
1667
1668 let section = unit.sections().next().unwrap();
1669 assert_eq!(section.get("RemainAfterExit"), Some("yes".to_string()));
1670 assert_eq!(section.get("PrivateTmp"), Some("no".to_string()));
1671 assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1672 assert_eq!(section.get_bool("PrivateTmp"), Some(false));
1673 }
1674}