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 children: Vec<_> = self.0.children_with_tokens().collect();
515 let insertion_index = children
516 .iter()
517 .enumerate()
518 .rev()
519 .find(|(_, child)| {
520 child.kind() != SyntaxKind::BLANK_LINE
521 && child.kind() != SyntaxKind::NEWLINE
522 && child.kind() != SyntaxKind::WHITESPACE
523 })
524 .map(|(idx, _)| idx + 1)
525 .unwrap_or(children.len());
526
527 self.0
528 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
529 }
530
531 pub fn add(&mut self, key: &str, value: &str) {
533 let new_entry = Entry::new(key, value);
534
535 let children: Vec<_> = self.0.children_with_tokens().collect();
537 let insertion_index = children
538 .iter()
539 .enumerate()
540 .rev()
541 .find(|(_, child)| {
542 child.kind() != SyntaxKind::BLANK_LINE
543 && child.kind() != SyntaxKind::NEWLINE
544 && child.kind() != SyntaxKind::WHITESPACE
545 })
546 .map(|(idx, _)| idx + 1)
547 .unwrap_or(children.len());
548
549 self.0
550 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
551 }
552
553 pub fn set_list(&mut self, key: &str, values: &[&str]) {
569 let value = values.join(" ");
570 self.set(key, &value);
571 }
572
573 pub fn get_list(&self, key: &str) -> Vec<String> {
578 self.entries()
579 .find(|e| e.key().as_deref() == Some(key))
580 .map(|e| e.value_as_list())
581 .unwrap_or_default()
582 }
583
584 pub fn get_bool(&self, key: &str) -> Option<bool> {
598 self.entries()
599 .find(|e| e.key().as_deref() == Some(key))
600 .and_then(|e| e.value_as_bool())
601 }
602
603 pub fn set_bool(&mut self, key: &str, value: bool) {
618 self.set(key, Entry::format_bool(value));
619 }
620
621 pub fn remove(&mut self, key: &str) {
623 let entry_to_remove = self.0.children().find_map(|child| {
625 let entry = Entry::cast(child)?;
626 if entry.key().as_deref() == Some(key) {
627 Some(entry)
628 } else {
629 None
630 }
631 });
632
633 if let Some(entry) = entry_to_remove {
634 entry.syntax().detach();
635 }
636 }
637
638 pub fn remove_all(&mut self, key: &str) {
640 let entries_to_remove: Vec<_> = self
642 .0
643 .children()
644 .filter_map(Entry::cast)
645 .filter(|e| e.key().as_deref() == Some(key))
646 .collect();
647
648 for entry in entries_to_remove {
649 entry.syntax().detach();
650 }
651 }
652
653 pub fn syntax(&self) -> &SyntaxNode {
655 &self.0
656 }
657}
658
659impl AstNode for Section {
660 type Language = Lang;
661
662 fn can_cast(kind: SyntaxKind) -> bool {
663 kind == SyntaxKind::SECTION
664 }
665
666 fn cast(node: SyntaxNode) -> Option<Self> {
667 if node.kind() == SyntaxKind::SECTION {
668 Some(Section(node))
669 } else {
670 None
671 }
672 }
673
674 fn syntax(&self) -> &SyntaxNode {
675 &self.0
676 }
677}
678
679fn unescape_string(s: &str) -> String {
681 let mut result = String::new();
682 let mut chars = s.chars().peekable();
683
684 while let Some(ch) = chars.next() {
685 if ch == '\\' {
686 match chars.next() {
687 Some('n') => result.push('\n'),
688 Some('t') => result.push('\t'),
689 Some('r') => result.push('\r'),
690 Some('\\') => result.push('\\'),
691 Some('"') => result.push('"'),
692 Some('\'') => result.push('\''),
693 Some('x') => {
694 let hex: String = chars.by_ref().take(2).collect();
696 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
697 result.push(byte as char);
698 } else {
699 result.push('\\');
701 result.push('x');
702 result.push_str(&hex);
703 }
704 }
705 Some('u') => {
706 let hex: String = chars.by_ref().take(4).collect();
708 if let Ok(code) = u32::from_str_radix(&hex, 16) {
709 if let Some(unicode_char) = char::from_u32(code) {
710 result.push(unicode_char);
711 } else {
712 result.push('\\');
714 result.push('u');
715 result.push_str(&hex);
716 }
717 } else {
718 result.push('\\');
720 result.push('u');
721 result.push_str(&hex);
722 }
723 }
724 Some('U') => {
725 let hex: String = chars.by_ref().take(8).collect();
727 if let Ok(code) = u32::from_str_radix(&hex, 16) {
728 if let Some(unicode_char) = char::from_u32(code) {
729 result.push(unicode_char);
730 } else {
731 result.push('\\');
733 result.push('U');
734 result.push_str(&hex);
735 }
736 } else {
737 result.push('\\');
739 result.push('U');
740 result.push_str(&hex);
741 }
742 }
743 Some(c) if c.is_ascii_digit() => {
744 let mut octal = String::from(c);
746 for _ in 0..2 {
747 if let Some(&next_ch) = chars.peek() {
748 if next_ch.is_ascii_digit() && next_ch < '8' {
749 octal.push(chars.next().unwrap());
750 } else {
751 break;
752 }
753 }
754 }
755 if let Ok(byte) = u8::from_str_radix(&octal, 8) {
756 result.push(byte as char);
757 } else {
758 result.push('\\');
760 result.push_str(&octal);
761 }
762 }
763 Some(c) => {
764 result.push('\\');
766 result.push(c);
767 }
768 None => {
769 result.push('\\');
771 }
772 }
773 } else {
774 result.push(ch);
775 }
776 }
777
778 result
779}
780
781fn escape_string(s: &str) -> String {
783 let mut result = String::new();
784
785 for ch in s.chars() {
786 match ch {
787 '\\' => result.push_str("\\\\"),
788 '\n' => result.push_str("\\n"),
789 '\t' => result.push_str("\\t"),
790 '\r' => result.push_str("\\r"),
791 '"' => result.push_str("\\\""),
792 _ => result.push(ch),
793 }
794 }
795
796 result
797}
798
799fn unquote_string(s: &str) -> String {
807 let trimmed = s.trim();
808
809 if trimmed.len() < 2 {
810 return trimmed.to_string();
811 }
812
813 let first = trimmed.chars().next();
814 let last = trimmed.chars().last();
815
816 if let (Some('"'), Some('"')) = (first, last) {
818 trimmed[1..trimmed.len() - 1].to_string()
820 } else if let (Some('\''), Some('\'')) = (first, last) {
821 trimmed[1..trimmed.len() - 1].to_string()
823 } else {
824 trimmed.to_string()
826 }
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, Hash)]
831pub struct Entry(SyntaxNode);
832
833impl Entry {
834 pub fn new(key: &str, value: &str) -> Entry {
836 use rowan::GreenNodeBuilder;
837
838 let mut builder = GreenNodeBuilder::new();
839 builder.start_node(SyntaxKind::ENTRY.into());
840 builder.token(SyntaxKind::KEY.into(), key);
841 builder.token(SyntaxKind::EQUALS.into(), "=");
842 builder.token(SyntaxKind::VALUE.into(), value);
843 builder.token(SyntaxKind::NEWLINE.into(), "\n");
844 builder.finish_node();
845 Entry(SyntaxNode::new_root_mut(builder.finish()))
846 }
847
848 pub fn key(&self) -> Option<String> {
850 let key_token = self
851 .0
852 .children_with_tokens()
853 .find(|e| e.kind() == SyntaxKind::KEY)?;
854 Some(key_token.as_token()?.text().to_string())
855 }
856
857 pub fn value(&self) -> Option<String> {
859 let mut found_equals = false;
861 let mut value_parts = Vec::new();
862
863 for element in self.0.children_with_tokens() {
864 match element.kind() {
865 SyntaxKind::EQUALS => found_equals = true,
866 SyntaxKind::VALUE if found_equals => {
867 value_parts.push(element.as_token()?.text().to_string());
868 }
869 SyntaxKind::LINE_CONTINUATION if found_equals => {
870 let should_add_space = value_parts
873 .last()
874 .map(|s| !s.ends_with(' ') && !s.ends_with('\t'))
875 .unwrap_or(true);
876 if should_add_space {
877 value_parts.push(" ".to_string());
878 }
879 }
880 SyntaxKind::WHITESPACE if found_equals && !value_parts.is_empty() => {
881 value_parts.push(element.as_token()?.text().to_string());
884 }
885 SyntaxKind::NEWLINE => break,
886 _ => {}
887 }
888 }
889
890 if value_parts.is_empty() {
891 None
892 } else {
893 Some(value_parts.join(""))
895 }
896 }
897
898 pub fn raw_value(&self) -> Option<String> {
900 let mut found_equals = false;
901 let mut value_parts = Vec::new();
902
903 for element in self.0.children_with_tokens() {
904 match element.kind() {
905 SyntaxKind::EQUALS => found_equals = true,
906 SyntaxKind::VALUE if found_equals => {
907 value_parts.push(element.as_token()?.text().to_string());
908 }
909 SyntaxKind::LINE_CONTINUATION if found_equals => {
910 value_parts.push(element.as_token()?.text().to_string());
911 }
912 SyntaxKind::WHITESPACE if found_equals => {
913 value_parts.push(element.as_token()?.text().to_string());
914 }
915 SyntaxKind::NEWLINE => break,
916 _ => {}
917 }
918 }
919
920 if value_parts.is_empty() {
921 None
922 } else {
923 Some(value_parts.join(""))
924 }
925 }
926
927 pub fn unescape_value(&self) -> Option<String> {
941 let value = self.value()?;
942 Some(unescape_string(&value))
943 }
944
945 pub fn escape_value(value: &str) -> String {
954 escape_string(value)
955 }
956
957 pub fn is_quoted(&self) -> Option<char> {
962 let value = self.value()?;
963 let trimmed = value.trim();
964
965 if trimmed.len() < 2 {
966 return None;
967 }
968
969 let first = trimmed.chars().next()?;
970 let last = trimmed.chars().last()?;
971
972 if (first == '"' || first == '\'') && first == last {
973 Some(first)
974 } else {
975 None
976 }
977 }
978
979 pub fn unquoted_value(&self) -> Option<String> {
984 let value = self.value()?;
985 Some(unquote_string(&value))
986 }
987
988 pub fn quoted_value(&self) -> Option<String> {
992 self.value()
994 }
995
996 pub fn value_as_list(&self) -> Vec<String> {
1004 let value = match self.unquoted_value() {
1005 Some(v) => v,
1006 None => return Vec::new(),
1007 };
1008
1009 value.split_whitespace().map(|s| s.to_string()).collect()
1010 }
1011
1012 pub fn value_as_bool(&self) -> Option<bool> {
1031 let value = self.unquoted_value()?;
1032 let value_lower = value.trim().to_lowercase();
1033
1034 match value_lower.as_str() {
1035 "1" | "yes" | "true" | "on" => Some(true),
1036 "0" | "no" | "false" | "off" => Some(false),
1037 _ => None,
1038 }
1039 }
1040
1041 pub fn format_bool(value: bool) -> &'static str {
1055 if value {
1056 "yes"
1057 } else {
1058 "no"
1059 }
1060 }
1061
1062 pub fn expand_specifiers(
1082 &self,
1083 context: &crate::specifier::SpecifierContext,
1084 ) -> Option<String> {
1085 let value = self.value()?;
1086 Some(context.expand(&value))
1087 }
1088
1089 pub fn syntax(&self) -> &SyntaxNode {
1091 &self.0
1092 }
1093}
1094
1095impl AstNode for Entry {
1096 type Language = Lang;
1097
1098 fn can_cast(kind: SyntaxKind) -> bool {
1099 kind == SyntaxKind::ENTRY
1100 }
1101
1102 fn cast(node: SyntaxNode) -> Option<Self> {
1103 if node.kind() == SyntaxKind::ENTRY {
1104 Some(Entry(node))
1105 } else {
1106 None
1107 }
1108 }
1109
1110 fn syntax(&self) -> &SyntaxNode {
1111 &self.0
1112 }
1113}
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118
1119 #[test]
1120 fn test_parse_simple() {
1121 let input = r#"[Unit]
1122Description=Test Service
1123After=network.target
1124"#;
1125 let unit = SystemdUnit::from_str(input).unwrap();
1126 assert_eq!(unit.sections().count(), 1);
1127
1128 let section = unit.sections().next().unwrap();
1129 assert_eq!(section.name(), Some("Unit".to_string()));
1130 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1131 assert_eq!(section.get("After"), Some("network.target".to_string()));
1132 }
1133
1134 #[test]
1135 fn test_parse_with_comments() {
1136 let input = r#"# Top comment
1137[Unit]
1138# Comment before description
1139Description=Test Service
1140; Semicolon comment
1141After=network.target
1142"#;
1143 let unit = SystemdUnit::from_str(input).unwrap();
1144 assert_eq!(unit.sections().count(), 1);
1145
1146 let section = unit.sections().next().unwrap();
1147 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1148 }
1149
1150 #[test]
1151 fn test_parse_multiple_sections() {
1152 let input = r#"[Unit]
1153Description=Test Service
1154
1155[Service]
1156Type=simple
1157ExecStart=/usr/bin/test
1158
1159[Install]
1160WantedBy=multi-user.target
1161"#;
1162 let unit = SystemdUnit::from_str(input).unwrap();
1163 assert_eq!(unit.sections().count(), 3);
1164
1165 let unit_section = unit.get_section("Unit").unwrap();
1166 assert_eq!(
1167 unit_section.get("Description"),
1168 Some("Test Service".to_string())
1169 );
1170
1171 let service_section = unit.get_section("Service").unwrap();
1172 assert_eq!(service_section.get("Type"), Some("simple".to_string()));
1173 assert_eq!(
1174 service_section.get("ExecStart"),
1175 Some("/usr/bin/test".to_string())
1176 );
1177
1178 let install_section = unit.get_section("Install").unwrap();
1179 assert_eq!(
1180 install_section.get("WantedBy"),
1181 Some("multi-user.target".to_string())
1182 );
1183 }
1184
1185 #[test]
1186 fn test_parse_with_spaces() {
1187 let input = "[Unit]\nDescription = Test Service\n";
1188 let unit = SystemdUnit::from_str(input).unwrap();
1189
1190 let section = unit.sections().next().unwrap();
1191 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1192 }
1193
1194 #[test]
1195 fn test_line_continuation() {
1196 let input = "[Service]\nExecStart=/bin/echo \\\n hello world\n";
1197 let unit = SystemdUnit::from_str(input).unwrap();
1198
1199 let section = unit.sections().next().unwrap();
1200 let entry = section.entries().next().unwrap();
1201 assert_eq!(entry.key(), Some("ExecStart".to_string()));
1202 assert_eq!(entry.value(), Some("/bin/echo hello world".to_string()));
1204 }
1205
1206 #[test]
1207 fn test_lossless_roundtrip() {
1208 let input = r#"# Comment
1209[Unit]
1210Description=Test Service
1211After=network.target
1212
1213[Service]
1214Type=simple
1215ExecStart=/usr/bin/test
1216"#;
1217 let unit = SystemdUnit::from_str(input).unwrap();
1218 let output = unit.text();
1219 assert_eq!(input, output);
1220 }
1221
1222 #[test]
1223 fn test_set_value() {
1224 let input = r#"[Unit]
1225Description=Test Service
1226"#;
1227 let unit = SystemdUnit::from_str(input).unwrap();
1228 {
1229 let mut section = unit.sections().next().unwrap();
1230 section.set("Description", "Updated Service");
1231 }
1232
1233 let section = unit.sections().next().unwrap();
1234 assert_eq!(
1235 section.get("Description"),
1236 Some("Updated Service".to_string())
1237 );
1238 }
1239
1240 #[test]
1241 fn test_add_new_entry() {
1242 let input = r#"[Unit]
1243Description=Test Service
1244"#;
1245 let unit = SystemdUnit::from_str(input).unwrap();
1246 {
1247 let mut section = unit.sections().next().unwrap();
1248 section.set("After", "network.target");
1249 }
1250
1251 let section = unit.sections().next().unwrap();
1252 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1253 assert_eq!(section.get("After"), Some("network.target".to_string()));
1254 }
1255
1256 #[test]
1257 fn test_multiple_values_same_key() {
1258 let input = r#"[Unit]
1259Wants=foo.service
1260Wants=bar.service
1261"#;
1262 let unit = SystemdUnit::from_str(input).unwrap();
1263 let section = unit.sections().next().unwrap();
1264
1265 assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
1267
1268 let all_wants = section.get_all("Wants");
1270 assert_eq!(all_wants.len(), 2);
1271 assert_eq!(all_wants[0], "foo.service");
1272 assert_eq!(all_wants[1], "bar.service");
1273 }
1274
1275 #[test]
1276 fn test_add_multiple_entries() {
1277 let input = r#"[Unit]
1278Description=Test Service
1279"#;
1280 let unit = SystemdUnit::from_str(input).unwrap();
1281 {
1282 let mut section = unit.sections().next().unwrap();
1283 section.add("Wants", "foo.service");
1284 section.add("Wants", "bar.service");
1285 }
1286
1287 let section = unit.sections().next().unwrap();
1288 let all_wants = section.get_all("Wants");
1289 assert_eq!(all_wants.len(), 2);
1290 assert_eq!(all_wants[0], "foo.service");
1291 assert_eq!(all_wants[1], "bar.service");
1292 }
1293
1294 #[test]
1295 fn test_remove_entry() {
1296 let input = r#"[Unit]
1297Description=Test Service
1298After=network.target
1299"#;
1300 let unit = SystemdUnit::from_str(input).unwrap();
1301 {
1302 let mut section = unit.sections().next().unwrap();
1303 section.remove("After");
1304 }
1305
1306 let section = unit.sections().next().unwrap();
1307 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1308 assert_eq!(section.get("After"), None);
1309 }
1310
1311 #[test]
1312 fn test_remove_all_entries() {
1313 let input = r#"[Unit]
1314Wants=foo.service
1315Wants=bar.service
1316Description=Test
1317"#;
1318 let unit = SystemdUnit::from_str(input).unwrap();
1319 {
1320 let mut section = unit.sections().next().unwrap();
1321 section.remove_all("Wants");
1322 }
1323
1324 let section = unit.sections().next().unwrap();
1325 assert_eq!(section.get_all("Wants").len(), 0);
1326 assert_eq!(section.get("Description"), Some("Test".to_string()));
1327 }
1328
1329 #[test]
1330 fn test_unescape_basic() {
1331 let input = r#"[Unit]
1332Description=Test\nService
1333"#;
1334 let unit = SystemdUnit::from_str(input).unwrap();
1335 let section = unit.sections().next().unwrap();
1336 let entry = section.entries().next().unwrap();
1337
1338 assert_eq!(entry.value(), Some("Test\\nService".to_string()));
1339 assert_eq!(entry.unescape_value(), Some("Test\nService".to_string()));
1340 }
1341
1342 #[test]
1343 fn test_unescape_all_escapes() {
1344 let input = r#"[Unit]
1345Value=\n\t\r\\\"\'\x41\101\u0041\U00000041
1346"#;
1347 let unit = SystemdUnit::from_str(input).unwrap();
1348 let section = unit.sections().next().unwrap();
1349 let entry = section.entries().next().unwrap();
1350
1351 let unescaped = entry.unescape_value().unwrap();
1352 assert_eq!(unescaped, "\n\t\r\\\"'AAAA");
1356 }
1357
1358 #[test]
1359 fn test_unescape_unicode() {
1360 let input = r#"[Unit]
1361Value=Hello\u0020World\U0001F44D
1362"#;
1363 let unit = SystemdUnit::from_str(input).unwrap();
1364 let section = unit.sections().next().unwrap();
1365 let entry = section.entries().next().unwrap();
1366
1367 let unescaped = entry.unescape_value().unwrap();
1368 assert_eq!(unescaped, "Hello World๐");
1370 }
1371
1372 #[test]
1373 fn test_escape_value() {
1374 let text = "Hello\nWorld\t\"Test\"\\Path";
1375 let escaped = Entry::escape_value(text);
1376 assert_eq!(escaped, "Hello\\nWorld\\t\\\"Test\\\"\\\\Path");
1377 }
1378
1379 #[test]
1380 fn test_escape_unescape_roundtrip() {
1381 let original = "Test\nwith\ttabs\rand\"quotes\"\\backslash";
1382 let escaped = Entry::escape_value(original);
1383 let unescaped = unescape_string(&escaped);
1384 assert_eq!(original, unescaped);
1385 }
1386
1387 #[test]
1388 fn test_unescape_invalid_sequences() {
1389 let input = r#"[Unit]
1391Value=\z\xFF\u12\U1234
1392"#;
1393 let unit = SystemdUnit::from_str(input).unwrap();
1394 let section = unit.sections().next().unwrap();
1395 let entry = section.entries().next().unwrap();
1396
1397 let unescaped = entry.unescape_value().unwrap();
1398 assert!(unescaped.contains("\\z"));
1400 }
1401
1402 #[test]
1403 fn test_quoted_double_quotes() {
1404 let input = r#"[Unit]
1405Description="Test Service"
1406"#;
1407 let unit = SystemdUnit::from_str(input).unwrap();
1408 let section = unit.sections().next().unwrap();
1409 let entry = section.entries().next().unwrap();
1410
1411 assert_eq!(entry.value(), Some("\"Test Service\"".to_string()));
1412 assert_eq!(entry.quoted_value(), Some("\"Test Service\"".to_string()));
1413 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1414 assert_eq!(entry.is_quoted(), Some('"'));
1415 }
1416
1417 #[test]
1418 fn test_quoted_single_quotes() {
1419 let input = r#"[Unit]
1420Description='Test Service'
1421"#;
1422 let unit = SystemdUnit::from_str(input).unwrap();
1423 let section = unit.sections().next().unwrap();
1424 let entry = section.entries().next().unwrap();
1425
1426 assert_eq!(entry.value(), Some("'Test Service'".to_string()));
1427 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1428 assert_eq!(entry.is_quoted(), Some('\''));
1429 }
1430
1431 #[test]
1432 fn test_quoted_with_whitespace() {
1433 let input = r#"[Unit]
1434Description=" Test Service "
1435"#;
1436 let unit = SystemdUnit::from_str(input).unwrap();
1437 let section = unit.sections().next().unwrap();
1438 let entry = section.entries().next().unwrap();
1439
1440 assert_eq!(entry.unquoted_value(), Some(" Test Service ".to_string()));
1442 }
1443
1444 #[test]
1445 fn test_unquoted_value() {
1446 let input = r#"[Unit]
1447Description=Test Service
1448"#;
1449 let unit = SystemdUnit::from_str(input).unwrap();
1450 let section = unit.sections().next().unwrap();
1451 let entry = section.entries().next().unwrap();
1452
1453 assert_eq!(entry.value(), Some("Test Service".to_string()));
1454 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1455 assert_eq!(entry.is_quoted(), None);
1456 }
1457
1458 #[test]
1459 fn test_mismatched_quotes() {
1460 let input = r#"[Unit]
1461Description="Test Service'
1462"#;
1463 let unit = SystemdUnit::from_str(input).unwrap();
1464 let section = unit.sections().next().unwrap();
1465 let entry = section.entries().next().unwrap();
1466
1467 assert_eq!(entry.is_quoted(), None);
1469 assert_eq!(entry.unquoted_value(), Some("\"Test Service'".to_string()));
1470 }
1471
1472 #[test]
1473 fn test_empty_quotes() {
1474 let input = r#"[Unit]
1475Description=""
1476"#;
1477 let unit = SystemdUnit::from_str(input).unwrap();
1478 let section = unit.sections().next().unwrap();
1479 let entry = section.entries().next().unwrap();
1480
1481 assert_eq!(entry.is_quoted(), Some('"'));
1482 assert_eq!(entry.unquoted_value(), Some("".to_string()));
1483 }
1484
1485 #[test]
1486 fn test_value_as_list() {
1487 let input = r#"[Unit]
1488After=network.target remote-fs.target
1489"#;
1490 let unit = SystemdUnit::from_str(input).unwrap();
1491 let section = unit.sections().next().unwrap();
1492 let entry = section.entries().next().unwrap();
1493
1494 let list = entry.value_as_list();
1495 assert_eq!(list.len(), 2);
1496 assert_eq!(list[0], "network.target");
1497 assert_eq!(list[1], "remote-fs.target");
1498 }
1499
1500 #[test]
1501 fn test_value_as_list_single() {
1502 let input = r#"[Unit]
1503After=network.target
1504"#;
1505 let unit = SystemdUnit::from_str(input).unwrap();
1506 let section = unit.sections().next().unwrap();
1507 let entry = section.entries().next().unwrap();
1508
1509 let list = entry.value_as_list();
1510 assert_eq!(list.len(), 1);
1511 assert_eq!(list[0], "network.target");
1512 }
1513
1514 #[test]
1515 fn test_value_as_list_empty() {
1516 let input = r#"[Unit]
1517After=
1518"#;
1519 let unit = SystemdUnit::from_str(input).unwrap();
1520 let section = unit.sections().next().unwrap();
1521 let entry = section.entries().next().unwrap();
1522
1523 let list = entry.value_as_list();
1524 assert_eq!(list.len(), 0);
1525 }
1526
1527 #[test]
1528 fn test_value_as_list_with_extra_whitespace() {
1529 let input = r#"[Unit]
1530After= network.target remote-fs.target
1531"#;
1532 let unit = SystemdUnit::from_str(input).unwrap();
1533 let section = unit.sections().next().unwrap();
1534 let entry = section.entries().next().unwrap();
1535
1536 let list = entry.value_as_list();
1537 assert_eq!(list.len(), 2);
1538 assert_eq!(list[0], "network.target");
1539 assert_eq!(list[1], "remote-fs.target");
1540 }
1541
1542 #[test]
1543 fn test_section_get_list() {
1544 let input = r#"[Unit]
1545After=network.target remote-fs.target
1546"#;
1547 let unit = SystemdUnit::from_str(input).unwrap();
1548 let section = unit.sections().next().unwrap();
1549
1550 let list = section.get_list("After");
1551 assert_eq!(list.len(), 2);
1552 assert_eq!(list[0], "network.target");
1553 assert_eq!(list[1], "remote-fs.target");
1554 }
1555
1556 #[test]
1557 fn test_section_get_list_missing() {
1558 let input = r#"[Unit]
1559Description=Test
1560"#;
1561 let unit = SystemdUnit::from_str(input).unwrap();
1562 let section = unit.sections().next().unwrap();
1563
1564 let list = section.get_list("After");
1565 assert_eq!(list.len(), 0);
1566 }
1567
1568 #[test]
1569 fn test_section_set_list() {
1570 let input = r#"[Unit]
1571Description=Test
1572"#;
1573 let unit = SystemdUnit::from_str(input).unwrap();
1574 {
1575 let mut section = unit.sections().next().unwrap();
1576 section.set_list("After", &["network.target", "remote-fs.target"]);
1577 }
1578
1579 let section = unit.sections().next().unwrap();
1580 let list = section.get_list("After");
1581 assert_eq!(list.len(), 2);
1582 assert_eq!(list[0], "network.target");
1583 assert_eq!(list[1], "remote-fs.target");
1584 }
1585
1586 #[test]
1587 fn test_section_set_list_replaces() {
1588 let input = r#"[Unit]
1589After=foo.target
1590"#;
1591 let unit = SystemdUnit::from_str(input).unwrap();
1592 {
1593 let mut section = unit.sections().next().unwrap();
1594 section.set_list("After", &["network.target", "remote-fs.target"]);
1595 }
1596
1597 let section = unit.sections().next().unwrap();
1598 let list = section.get_list("After");
1599 assert_eq!(list.len(), 2);
1600 assert_eq!(list[0], "network.target");
1601 assert_eq!(list[1], "remote-fs.target");
1602 }
1603
1604 #[test]
1605 fn test_value_as_bool_positive() {
1606 let inputs = vec!["yes", "true", "1", "on", "YES", "True", "ON"];
1607
1608 for input_val in inputs {
1609 let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1610 let unit = SystemdUnit::from_str(&input).unwrap();
1611 let section = unit.sections().next().unwrap();
1612 let entry = section.entries().next().unwrap();
1613 assert_eq!(
1614 entry.value_as_bool(),
1615 Some(true),
1616 "Failed for input: {}",
1617 input_val
1618 );
1619 }
1620 }
1621
1622 #[test]
1623 fn test_value_as_bool_negative() {
1624 let inputs = vec!["no", "false", "0", "off", "NO", "False", "OFF"];
1625
1626 for input_val in inputs {
1627 let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1628 let unit = SystemdUnit::from_str(&input).unwrap();
1629 let section = unit.sections().next().unwrap();
1630 let entry = section.entries().next().unwrap();
1631 assert_eq!(
1632 entry.value_as_bool(),
1633 Some(false),
1634 "Failed for input: {}",
1635 input_val
1636 );
1637 }
1638 }
1639
1640 #[test]
1641 fn test_value_as_bool_invalid() {
1642 let input = r#"[Service]
1643RemainAfterExit=maybe
1644"#;
1645 let unit = SystemdUnit::from_str(input).unwrap();
1646 let section = unit.sections().next().unwrap();
1647 let entry = section.entries().next().unwrap();
1648 assert_eq!(entry.value_as_bool(), None);
1649 }
1650
1651 #[test]
1652 fn test_value_as_bool_with_whitespace() {
1653 let input = r#"[Service]
1654RemainAfterExit= yes
1655"#;
1656 let unit = SystemdUnit::from_str(input).unwrap();
1657 let section = unit.sections().next().unwrap();
1658 let entry = section.entries().next().unwrap();
1659 assert_eq!(entry.value_as_bool(), Some(true));
1660 }
1661
1662 #[test]
1663 fn test_format_bool() {
1664 assert_eq!(Entry::format_bool(true), "yes");
1665 assert_eq!(Entry::format_bool(false), "no");
1666 }
1667
1668 #[test]
1669 fn test_section_get_bool() {
1670 let input = r#"[Service]
1671RemainAfterExit=yes
1672Type=simple
1673"#;
1674 let unit = SystemdUnit::from_str(input).unwrap();
1675 let section = unit.sections().next().unwrap();
1676
1677 assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1678 assert_eq!(section.get_bool("Type"), None); assert_eq!(section.get_bool("Missing"), None); }
1681
1682 #[test]
1683 fn test_section_set_bool() {
1684 let input = r#"[Service]
1685Type=simple
1686"#;
1687 let unit = SystemdUnit::from_str(input).unwrap();
1688 {
1689 let mut section = unit.sections().next().unwrap();
1690 section.set_bool("RemainAfterExit", true);
1691 section.set_bool("PrivateTmp", false);
1692 }
1693
1694 let section = unit.sections().next().unwrap();
1695 assert_eq!(section.get("RemainAfterExit"), Some("yes".to_string()));
1696 assert_eq!(section.get("PrivateTmp"), Some("no".to_string()));
1697 assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1698 assert_eq!(section.get_bool("PrivateTmp"), Some(false));
1699 }
1700
1701 #[test]
1702 fn test_add_entry_with_trailing_whitespace() {
1703 let input = r#"[Unit]
1705Description=Test Service
1706
1707"#;
1708 let unit = SystemdUnit::from_str(input).unwrap();
1709 {
1710 let mut section = unit.sections().next().unwrap();
1711 section.add("After", "network.target");
1712 }
1713
1714 let output = unit.text();
1715 let expected = r#"[Unit]
1717Description=Test Service
1718After=network.target
1719
1720"#;
1721 assert_eq!(output, expected);
1722 }
1723
1724 #[test]
1725 fn test_set_new_entry_with_trailing_whitespace() {
1726 let input = r#"[Unit]
1728Description=Test Service
1729
1730"#;
1731 let unit = SystemdUnit::from_str(input).unwrap();
1732 {
1733 let mut section = unit.sections().next().unwrap();
1734 section.set("After", "network.target");
1735 }
1736
1737 let output = unit.text();
1738 let expected = r#"[Unit]
1740Description=Test Service
1741After=network.target
1742
1743"#;
1744 assert_eq!(output, expected);
1745 }
1746}