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 => {
285 self.skip_blank_lines();
286 }
287 SyntaxKind::WHITESPACE => {
288 let pos_before = self.pos;
291 self.skip_blank_lines();
292 if self.pos == pos_before {
293 self.errors.push("unexpected whitespace at start of line (should be indented continuation or blank line)".to_string());
296 self.bump();
297 }
298 }
299 _ => {
300 self.errors
301 .push(format!("unexpected token in section: {:?}", kind));
302 self.bump();
303 }
304 }
305 }
306
307 self.builder.finish_node();
308 }
309
310 fn parse_file(&mut self) {
311 self.builder.start_node(SyntaxKind::ROOT.into());
312
313 while let Some(kind) = self.current() {
315 match kind {
316 SyntaxKind::COMMENT => {
317 self.builder.start_node(SyntaxKind::ENTRY.into());
318 self.bump();
319 if self.current() == Some(SyntaxKind::NEWLINE) {
320 self.bump();
321 }
322 self.builder.finish_node();
323 }
324 SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE => {
325 self.skip_blank_lines();
326 }
327 _ => break,
328 }
329 }
330
331 while self.current().is_some() {
333 if self.current() == Some(SyntaxKind::LEFT_BRACKET) {
334 self.parse_section();
335 } else {
336 self.errors
337 .push(format!("expected section header, got {:?}", self.current()));
338 self.bump();
339 }
340 }
341
342 self.builder.finish_node();
343 }
344 }
345
346 let mut tokens: Vec<_> = lex(text).collect();
347 tokens.reverse();
348
349 let mut parser = Parser {
350 tokens,
351 builder: GreenNodeBuilder::new(),
352 errors: Vec::new(),
353 positioned_errors: Vec::new(),
354 pos: 0,
355 };
356
357 parser.parse_file();
358
359 ParseResult {
360 green_node: parser.builder.finish(),
361 errors: parser.errors,
362 positioned_errors: parser.positioned_errors,
363 }
364}
365
366type SyntaxNode = rowan::SyntaxNode<Lang>;
368
369fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
372 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
373 let mut line = 0;
374 let mut last_newline_offset = rowan::TextSize::from(0);
375
376 for element in root.preorder_with_tokens() {
377 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
378 if token.text_range().start() >= offset {
379 break;
380 }
381
382 for (idx, _) in token.text().match_indices('\n') {
384 line += 1;
385 last_newline_offset =
386 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
387 }
388 }
389 }
390
391 let column: usize = (offset - last_newline_offset).into();
392 (line, column)
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Hash)]
397pub struct SystemdUnit(SyntaxNode);
398
399impl SystemdUnit {
400 pub fn sections(&self) -> impl Iterator<Item = Section> {
402 self.0.children().filter_map(Section::cast)
403 }
404
405 pub fn get_section(&self, name: &str) -> Option<Section> {
407 self.sections().find(|s| s.name().as_deref() == Some(name))
408 }
409
410 pub fn add_section(&mut self, name: &str) {
412 let new_section = Section::new(name);
413 let insertion_index = self.0.children_with_tokens().count();
414 self.0
415 .splice_children(insertion_index..insertion_index, vec![new_section.0.into()]);
416 }
417
418 pub fn syntax(&self) -> &SyntaxNode {
420 &self.0
421 }
422
423 pub fn text(&self) -> String {
425 self.0.text().to_string()
426 }
427
428 pub fn from_file(path: &Path) -> Result<Self, Error> {
430 let text = std::fs::read_to_string(path)?;
431 Self::from_str(&text)
432 }
433
434 pub fn write_to_file(&self, path: &Path) -> Result<(), Error> {
436 std::fs::write(path, self.text())?;
437 Ok(())
438 }
439
440 pub fn line(&self) -> usize {
442 line_col_at_offset(&self.0, self.0.text_range().start()).0
443 }
444
445 pub fn column(&self) -> usize {
447 line_col_at_offset(&self.0, self.0.text_range().start()).1
448 }
449
450 pub fn line_col(&self) -> (usize, usize) {
453 line_col_at_offset(&self.0, self.0.text_range().start())
454 }
455}
456
457impl AstNode for SystemdUnit {
458 type Language = Lang;
459
460 fn can_cast(kind: SyntaxKind) -> bool {
461 kind == SyntaxKind::ROOT
462 }
463
464 fn cast(node: SyntaxNode) -> Option<Self> {
465 if node.kind() == SyntaxKind::ROOT {
466 Some(SystemdUnit(node))
467 } else {
468 None
469 }
470 }
471
472 fn syntax(&self) -> &SyntaxNode {
473 &self.0
474 }
475}
476
477impl FromStr for SystemdUnit {
478 type Err = Error;
479
480 fn from_str(s: &str) -> Result<Self, Self::Err> {
481 let parsed = parse(s);
482 if !parsed.errors.is_empty() {
483 return Err(Error::ParseError(ParseError(parsed.errors)));
484 }
485 let node = SyntaxNode::new_root_mut(parsed.green_node);
486 Ok(SystemdUnit::cast(node).expect("root node should be SystemdUnit"))
487 }
488}
489
490impl std::fmt::Display for SystemdUnit {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 write!(f, "{}", self.0.text())
493 }
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Hash)]
498pub struct Section(SyntaxNode);
499
500impl Section {
501 pub fn new(name: &str) -> Section {
503 use rowan::GreenNodeBuilder;
504
505 let mut builder = GreenNodeBuilder::new();
506 builder.start_node(SyntaxKind::SECTION.into());
507
508 builder.start_node(SyntaxKind::SECTION_HEADER.into());
510 builder.token(SyntaxKind::LEFT_BRACKET.into(), "[");
511 builder.token(SyntaxKind::SECTION_NAME.into(), name);
512 builder.token(SyntaxKind::RIGHT_BRACKET.into(), "]");
513 builder.token(SyntaxKind::NEWLINE.into(), "\n");
514 builder.finish_node();
515
516 builder.finish_node();
517 Section(SyntaxNode::new_root_mut(builder.finish()))
518 }
519
520 pub fn name(&self) -> Option<String> {
522 let header = self
523 .0
524 .children()
525 .find(|n| n.kind() == SyntaxKind::SECTION_HEADER)?;
526 let value = header
527 .children_with_tokens()
528 .find(|e| e.kind() == SyntaxKind::SECTION_NAME)?;
529 Some(value.as_token()?.text().to_string())
530 }
531
532 pub fn entries(&self) -> impl Iterator<Item = Entry> {
534 self.0.children().filter_map(Entry::cast)
535 }
536
537 pub fn get(&self, key: &str) -> Option<String> {
539 self.entries()
540 .find(|e| e.key().as_deref() == Some(key))
541 .and_then(|e| e.value())
542 }
543
544 pub fn get_all(&self, key: &str) -> Vec<String> {
546 self.entries()
547 .filter(|e| e.key().as_deref() == Some(key))
548 .filter_map(|e| e.value())
549 .collect()
550 }
551
552 pub fn set(&mut self, key: &str, value: &str) {
554 let new_entry = Entry::new(key, value);
555
556 for entry in self.entries() {
558 if entry.key().as_deref() == Some(key) {
559 self.0.splice_children(
560 entry.0.index()..entry.0.index() + 1,
561 vec![new_entry.0.into()],
562 );
563 return;
564 }
565 }
566
567 let children: Vec<_> = self.0.children_with_tokens().collect();
569 let insertion_index = children
570 .iter()
571 .enumerate()
572 .rev()
573 .find(|(_, child)| {
574 child.kind() != SyntaxKind::BLANK_LINE
575 && child.kind() != SyntaxKind::NEWLINE
576 && child.kind() != SyntaxKind::WHITESPACE
577 })
578 .map(|(idx, _)| idx + 1)
579 .unwrap_or(children.len());
580
581 self.0
582 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
583 }
584
585 pub fn add(&mut self, key: &str, value: &str) {
587 let new_entry = Entry::new(key, value);
588
589 let children: Vec<_> = self.0.children_with_tokens().collect();
591 let insertion_index = children
592 .iter()
593 .enumerate()
594 .rev()
595 .find(|(_, child)| {
596 child.kind() != SyntaxKind::BLANK_LINE
597 && child.kind() != SyntaxKind::NEWLINE
598 && child.kind() != SyntaxKind::WHITESPACE
599 })
600 .map(|(idx, _)| idx + 1)
601 .unwrap_or(children.len());
602
603 self.0
604 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
605 }
606
607 pub fn insert_at(&mut self, index: usize, key: &str, value: &str) {
634 let new_entry = Entry::new(key, value);
635
636 let entries: Vec<_> = self.entries().collect();
638
639 if index >= entries.len() {
640 self.add(key, value);
642 } else {
643 let target_entry = &entries[index];
645 let insertion_index = target_entry.0.index();
646 self.0
647 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
648 }
649 }
650
651 pub fn insert_before(&mut self, existing_key: &str, key: &str, value: &str) {
677 let new_entry = Entry::new(key, value);
678
679 let target_entry = self
681 .entries()
682 .find(|e| e.key().as_deref() == Some(existing_key));
683
684 if let Some(entry) = target_entry {
685 let insertion_index = entry.0.index();
686 self.0
687 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
688 }
689 }
691
692 pub fn insert_after(&mut self, existing_key: &str, key: &str, value: &str) {
718 let new_entry = Entry::new(key, value);
719
720 let target_entry = self
722 .entries()
723 .find(|e| e.key().as_deref() == Some(existing_key));
724
725 if let Some(entry) = target_entry {
726 let insertion_index = entry.0.index() + 1;
727 self.0
728 .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
729 }
730 }
732
733 pub fn set_list(&mut self, key: &str, values: &[&str]) {
749 let value = values.join(" ");
750 self.set(key, &value);
751 }
752
753 pub fn get_list(&self, key: &str) -> Vec<String> {
758 self.entries()
759 .find(|e| e.key().as_deref() == Some(key))
760 .map(|e| e.value_as_list())
761 .unwrap_or_default()
762 }
763
764 pub fn get_bool(&self, key: &str) -> Option<bool> {
778 self.entries()
779 .find(|e| e.key().as_deref() == Some(key))
780 .and_then(|e| e.value_as_bool())
781 }
782
783 pub fn set_bool(&mut self, key: &str, value: bool) {
798 self.set(key, Entry::format_bool(value));
799 }
800
801 pub fn remove(&mut self, key: &str) {
803 let entry_to_remove = self.0.children().find_map(|child| {
805 let entry = Entry::cast(child)?;
806 if entry.key().as_deref() == Some(key) {
807 Some(entry)
808 } else {
809 None
810 }
811 });
812
813 if let Some(entry) = entry_to_remove {
814 entry.syntax().detach();
815 }
816 }
817
818 pub fn remove_all(&mut self, key: &str) {
820 let entries_to_remove: Vec<_> = self
822 .0
823 .children()
824 .filter_map(Entry::cast)
825 .filter(|e| e.key().as_deref() == Some(key))
826 .collect();
827
828 for entry in entries_to_remove {
829 entry.syntax().detach();
830 }
831 }
832
833 pub fn remove_value(&mut self, key: &str, value_to_remove: &str) {
861 let entries_to_process: Vec<_> = self
863 .entries()
864 .filter(|e| e.key().as_deref() == Some(key))
865 .collect();
866
867 for entry in entries_to_process {
868 let current_list = entry.value_as_list();
870
871 let new_list: Vec<_> = current_list
873 .iter()
874 .filter(|v| v.as_str() != value_to_remove)
875 .map(|s| s.as_str())
876 .collect();
877
878 if new_list.is_empty() {
879 entry.syntax().detach();
881 } else if new_list.len() < current_list.len() {
882 let new_entry = Entry::new(key, &new_list.join(" "));
885
886 let index = entry.0.index();
888 self.0
889 .splice_children(index..index + 1, vec![new_entry.0.into()]);
890 }
891 }
893 }
894
895 pub fn remove_entries_where<F>(&mut self, mut predicate: F)
926 where
927 F: FnMut(&str, &str) -> bool,
928 {
929 let entries_to_remove: Vec<_> = self
931 .entries()
932 .filter(|entry| {
933 if let (Some(key), Some(value)) = (entry.key(), entry.value()) {
934 predicate(&key, &value)
935 } else {
936 false
937 }
938 })
939 .collect();
940
941 for entry in entries_to_remove {
942 entry.syntax().detach();
943 }
944 }
945
946 pub fn syntax(&self) -> &SyntaxNode {
948 &self.0
949 }
950
951 pub fn line(&self) -> usize {
953 line_col_at_offset(&self.0, self.0.text_range().start()).0
954 }
955
956 pub fn column(&self) -> usize {
958 line_col_at_offset(&self.0, self.0.text_range().start()).1
959 }
960
961 pub fn line_col(&self) -> (usize, usize) {
964 line_col_at_offset(&self.0, self.0.text_range().start())
965 }
966}
967
968impl AstNode for Section {
969 type Language = Lang;
970
971 fn can_cast(kind: SyntaxKind) -> bool {
972 kind == SyntaxKind::SECTION
973 }
974
975 fn cast(node: SyntaxNode) -> Option<Self> {
976 if node.kind() == SyntaxKind::SECTION {
977 Some(Section(node))
978 } else {
979 None
980 }
981 }
982
983 fn syntax(&self) -> &SyntaxNode {
984 &self.0
985 }
986}
987
988fn unescape_string(s: &str) -> String {
990 let mut result = String::new();
991 let mut chars = s.chars().peekable();
992
993 while let Some(ch) = chars.next() {
994 if ch == '\\' {
995 match chars.next() {
996 Some('n') => result.push('\n'),
997 Some('t') => result.push('\t'),
998 Some('r') => result.push('\r'),
999 Some('\\') => result.push('\\'),
1000 Some('"') => result.push('"'),
1001 Some('\'') => result.push('\''),
1002 Some('x') => {
1003 let hex: String = chars.by_ref().take(2).collect();
1005 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
1006 result.push(byte as char);
1007 } else {
1008 result.push('\\');
1010 result.push('x');
1011 result.push_str(&hex);
1012 }
1013 }
1014 Some('u') => {
1015 let hex: String = chars.by_ref().take(4).collect();
1017 if let Ok(code) = u32::from_str_radix(&hex, 16) {
1018 if let Some(unicode_char) = char::from_u32(code) {
1019 result.push(unicode_char);
1020 } else {
1021 result.push('\\');
1023 result.push('u');
1024 result.push_str(&hex);
1025 }
1026 } else {
1027 result.push('\\');
1029 result.push('u');
1030 result.push_str(&hex);
1031 }
1032 }
1033 Some('U') => {
1034 let hex: String = chars.by_ref().take(8).collect();
1036 if let Ok(code) = u32::from_str_radix(&hex, 16) {
1037 if let Some(unicode_char) = char::from_u32(code) {
1038 result.push(unicode_char);
1039 } else {
1040 result.push('\\');
1042 result.push('U');
1043 result.push_str(&hex);
1044 }
1045 } else {
1046 result.push('\\');
1048 result.push('U');
1049 result.push_str(&hex);
1050 }
1051 }
1052 Some(c) if c.is_ascii_digit() => {
1053 let mut octal = String::from(c);
1055 for _ in 0..2 {
1056 if let Some(&next_ch) = chars.peek() {
1057 if next_ch.is_ascii_digit() && next_ch < '8' {
1058 octal.push(chars.next().unwrap());
1059 } else {
1060 break;
1061 }
1062 }
1063 }
1064 if let Ok(byte) = u8::from_str_radix(&octal, 8) {
1065 result.push(byte as char);
1066 } else {
1067 result.push('\\');
1069 result.push_str(&octal);
1070 }
1071 }
1072 Some(c) => {
1073 result.push('\\');
1075 result.push(c);
1076 }
1077 None => {
1078 result.push('\\');
1080 }
1081 }
1082 } else {
1083 result.push(ch);
1084 }
1085 }
1086
1087 result
1088}
1089
1090fn escape_string(s: &str) -> String {
1092 let mut result = String::new();
1093
1094 for ch in s.chars() {
1095 match ch {
1096 '\\' => result.push_str("\\\\"),
1097 '\n' => result.push_str("\\n"),
1098 '\t' => result.push_str("\\t"),
1099 '\r' => result.push_str("\\r"),
1100 '"' => result.push_str("\\\""),
1101 _ => result.push(ch),
1102 }
1103 }
1104
1105 result
1106}
1107
1108fn unquote_string(s: &str) -> String {
1116 let trimmed = s.trim();
1117
1118 if trimmed.len() < 2 {
1119 return trimmed.to_string();
1120 }
1121
1122 let first = trimmed.chars().next();
1123 let last = trimmed.chars().last();
1124
1125 if let (Some('"'), Some('"')) = (first, last) {
1127 trimmed[1..trimmed.len() - 1].to_string()
1129 } else if let (Some('\''), Some('\'')) = (first, last) {
1130 trimmed[1..trimmed.len() - 1].to_string()
1132 } else {
1133 trimmed.to_string()
1135 }
1136}
1137
1138#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1140pub struct Entry(SyntaxNode);
1141
1142impl Entry {
1143 pub fn new(key: &str, value: &str) -> Entry {
1145 use rowan::GreenNodeBuilder;
1146
1147 let mut builder = GreenNodeBuilder::new();
1148 builder.start_node(SyntaxKind::ENTRY.into());
1149 builder.token(SyntaxKind::KEY.into(), key);
1150 builder.token(SyntaxKind::EQUALS.into(), "=");
1151 builder.token(SyntaxKind::VALUE.into(), value);
1152 builder.token(SyntaxKind::NEWLINE.into(), "\n");
1153 builder.finish_node();
1154 Entry(SyntaxNode::new_root_mut(builder.finish()))
1155 }
1156
1157 pub fn key(&self) -> Option<String> {
1159 let key_token = self
1160 .0
1161 .children_with_tokens()
1162 .find(|e| e.kind() == SyntaxKind::KEY)?;
1163 Some(key_token.as_token()?.text().to_string())
1164 }
1165
1166 pub fn value(&self) -> Option<String> {
1168 let mut found_equals = false;
1170 let mut value_parts = Vec::new();
1171
1172 for element in self.0.children_with_tokens() {
1173 match element.kind() {
1174 SyntaxKind::EQUALS => found_equals = true,
1175 SyntaxKind::VALUE if found_equals => {
1176 value_parts.push(element.as_token()?.text().to_string());
1177 }
1178 SyntaxKind::LINE_CONTINUATION if found_equals => {
1179 let should_add_space = value_parts
1182 .last()
1183 .map(|s| !s.ends_with(' ') && !s.ends_with('\t'))
1184 .unwrap_or(true);
1185 if should_add_space {
1186 value_parts.push(" ".to_string());
1187 }
1188 }
1189 SyntaxKind::WHITESPACE if found_equals && !value_parts.is_empty() => {
1190 value_parts.push(element.as_token()?.text().to_string());
1193 }
1194 SyntaxKind::NEWLINE => break,
1195 _ => {}
1196 }
1197 }
1198
1199 if value_parts.is_empty() {
1200 None
1201 } else {
1202 Some(value_parts.join(""))
1204 }
1205 }
1206
1207 pub fn raw_value(&self) -> Option<String> {
1209 let mut found_equals = false;
1210 let mut value_parts = Vec::new();
1211
1212 for element in self.0.children_with_tokens() {
1213 match element.kind() {
1214 SyntaxKind::EQUALS => found_equals = true,
1215 SyntaxKind::VALUE if found_equals => {
1216 value_parts.push(element.as_token()?.text().to_string());
1217 }
1218 SyntaxKind::LINE_CONTINUATION if found_equals => {
1219 value_parts.push(element.as_token()?.text().to_string());
1220 }
1221 SyntaxKind::WHITESPACE if found_equals => {
1222 value_parts.push(element.as_token()?.text().to_string());
1223 }
1224 SyntaxKind::NEWLINE => break,
1225 _ => {}
1226 }
1227 }
1228
1229 if value_parts.is_empty() {
1230 None
1231 } else {
1232 Some(value_parts.join(""))
1233 }
1234 }
1235
1236 pub fn unescape_value(&self) -> Option<String> {
1250 let value = self.value()?;
1251 Some(unescape_string(&value))
1252 }
1253
1254 pub fn escape_value(value: &str) -> String {
1263 escape_string(value)
1264 }
1265
1266 pub fn is_quoted(&self) -> Option<char> {
1271 let value = self.value()?;
1272 let trimmed = value.trim();
1273
1274 if trimmed.len() < 2 {
1275 return None;
1276 }
1277
1278 let first = trimmed.chars().next()?;
1279 let last = trimmed.chars().last()?;
1280
1281 if (first == '"' || first == '\'') && first == last {
1282 Some(first)
1283 } else {
1284 None
1285 }
1286 }
1287
1288 pub fn unquoted_value(&self) -> Option<String> {
1293 let value = self.value()?;
1294 Some(unquote_string(&value))
1295 }
1296
1297 pub fn quoted_value(&self) -> Option<String> {
1301 self.value()
1303 }
1304
1305 pub fn value_as_list(&self) -> Vec<String> {
1313 let value = match self.unquoted_value() {
1314 Some(v) => v,
1315 None => return Vec::new(),
1316 };
1317
1318 value.split_whitespace().map(|s| s.to_string()).collect()
1319 }
1320
1321 pub fn value_as_bool(&self) -> Option<bool> {
1340 let value = self.unquoted_value()?;
1341 let value_lower = value.trim().to_lowercase();
1342
1343 match value_lower.as_str() {
1344 "1" | "yes" | "true" | "on" => Some(true),
1345 "0" | "no" | "false" | "off" => Some(false),
1346 _ => None,
1347 }
1348 }
1349
1350 pub fn format_bool(value: bool) -> &'static str {
1364 if value {
1365 "yes"
1366 } else {
1367 "no"
1368 }
1369 }
1370
1371 pub fn expand_specifiers(
1391 &self,
1392 context: &crate::specifier::SpecifierContext,
1393 ) -> Option<String> {
1394 let value = self.value()?;
1395 Some(context.expand(&value))
1396 }
1397
1398 pub fn set_value(&self, new_value: &str) {
1432 let key = self.key().expect("Entry should have a key");
1433 let new_entry = Entry::new(&key, new_value);
1434
1435 let parent = self.0.parent().expect("Entry should have a parent");
1437 let index = self.0.index();
1438 parent.splice_children(index..index + 1, vec![new_entry.0.into()]);
1439 }
1440
1441 pub fn syntax(&self) -> &SyntaxNode {
1443 &self.0
1444 }
1445
1446 pub fn line(&self) -> usize {
1448 line_col_at_offset(&self.0, self.0.text_range().start()).0
1449 }
1450
1451 pub fn column(&self) -> usize {
1453 line_col_at_offset(&self.0, self.0.text_range().start()).1
1454 }
1455
1456 pub fn line_col(&self) -> (usize, usize) {
1459 line_col_at_offset(&self.0, self.0.text_range().start())
1460 }
1461}
1462
1463impl AstNode for Entry {
1464 type Language = Lang;
1465
1466 fn can_cast(kind: SyntaxKind) -> bool {
1467 kind == SyntaxKind::ENTRY
1468 }
1469
1470 fn cast(node: SyntaxNode) -> Option<Self> {
1471 if node.kind() == SyntaxKind::ENTRY {
1472 Some(Entry(node))
1473 } else {
1474 None
1475 }
1476 }
1477
1478 fn syntax(&self) -> &SyntaxNode {
1479 &self.0
1480 }
1481}
1482
1483#[cfg(test)]
1484mod tests {
1485 use super::*;
1486
1487 #[test]
1488 fn test_parse_simple() {
1489 let input = r#"[Unit]
1490Description=Test Service
1491After=network.target
1492"#;
1493 let unit = SystemdUnit::from_str(input).unwrap();
1494 assert_eq!(unit.sections().count(), 1);
1495
1496 let section = unit.sections().next().unwrap();
1497 assert_eq!(section.name(), Some("Unit".to_string()));
1498 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1499 assert_eq!(section.get("After"), Some("network.target".to_string()));
1500 }
1501
1502 #[test]
1503 fn test_parse_with_comments() {
1504 let input = r#"# Top comment
1505[Unit]
1506# Comment before description
1507Description=Test Service
1508; Semicolon comment
1509After=network.target
1510"#;
1511 let unit = SystemdUnit::from_str(input).unwrap();
1512 assert_eq!(unit.sections().count(), 1);
1513
1514 let section = unit.sections().next().unwrap();
1515 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1516 }
1517
1518 #[test]
1519 fn test_parse_multiple_sections() {
1520 let input = r#"[Unit]
1521Description=Test Service
1522
1523[Service]
1524Type=simple
1525ExecStart=/usr/bin/test
1526
1527[Install]
1528WantedBy=multi-user.target
1529"#;
1530 let unit = SystemdUnit::from_str(input).unwrap();
1531 assert_eq!(unit.sections().count(), 3);
1532
1533 let unit_section = unit.get_section("Unit").unwrap();
1534 assert_eq!(
1535 unit_section.get("Description"),
1536 Some("Test Service".to_string())
1537 );
1538
1539 let service_section = unit.get_section("Service").unwrap();
1540 assert_eq!(service_section.get("Type"), Some("simple".to_string()));
1541 assert_eq!(
1542 service_section.get("ExecStart"),
1543 Some("/usr/bin/test".to_string())
1544 );
1545
1546 let install_section = unit.get_section("Install").unwrap();
1547 assert_eq!(
1548 install_section.get("WantedBy"),
1549 Some("multi-user.target".to_string())
1550 );
1551 }
1552
1553 #[test]
1554 fn test_parse_with_spaces() {
1555 let input = "[Unit]\nDescription = Test Service\n";
1556 let unit = SystemdUnit::from_str(input).unwrap();
1557
1558 let section = unit.sections().next().unwrap();
1559 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1560 }
1561
1562 #[test]
1563 fn test_line_continuation() {
1564 let input = "[Service]\nExecStart=/bin/echo \\\n hello world\n";
1565 let unit = SystemdUnit::from_str(input).unwrap();
1566
1567 let section = unit.sections().next().unwrap();
1568 let entry = section.entries().next().unwrap();
1569 assert_eq!(entry.key(), Some("ExecStart".to_string()));
1570 assert_eq!(entry.value(), Some("/bin/echo hello world".to_string()));
1572 }
1573
1574 #[test]
1575 fn test_lossless_roundtrip() {
1576 let input = r#"# Comment
1577[Unit]
1578Description=Test Service
1579After=network.target
1580
1581[Service]
1582Type=simple
1583ExecStart=/usr/bin/test
1584"#;
1585 let unit = SystemdUnit::from_str(input).unwrap();
1586 let output = unit.text();
1587 assert_eq!(input, output);
1588 }
1589
1590 #[test]
1591 fn test_set_value() {
1592 let input = r#"[Unit]
1593Description=Test Service
1594"#;
1595 let unit = SystemdUnit::from_str(input).unwrap();
1596 {
1597 let mut section = unit.sections().next().unwrap();
1598 section.set("Description", "Updated Service");
1599 }
1600
1601 let section = unit.sections().next().unwrap();
1602 assert_eq!(
1603 section.get("Description"),
1604 Some("Updated Service".to_string())
1605 );
1606 }
1607
1608 #[test]
1609 fn test_add_new_entry() {
1610 let input = r#"[Unit]
1611Description=Test Service
1612"#;
1613 let unit = SystemdUnit::from_str(input).unwrap();
1614 {
1615 let mut section = unit.sections().next().unwrap();
1616 section.set("After", "network.target");
1617 }
1618
1619 let section = unit.sections().next().unwrap();
1620 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1621 assert_eq!(section.get("After"), Some("network.target".to_string()));
1622 }
1623
1624 #[test]
1625 fn test_multiple_values_same_key() {
1626 let input = r#"[Unit]
1627Wants=foo.service
1628Wants=bar.service
1629"#;
1630 let unit = SystemdUnit::from_str(input).unwrap();
1631 let section = unit.sections().next().unwrap();
1632
1633 assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
1635
1636 let all_wants = section.get_all("Wants");
1638 assert_eq!(all_wants.len(), 2);
1639 assert_eq!(all_wants[0], "foo.service");
1640 assert_eq!(all_wants[1], "bar.service");
1641 }
1642
1643 #[test]
1644 fn test_add_multiple_entries() {
1645 let input = r#"[Unit]
1646Description=Test Service
1647"#;
1648 let unit = SystemdUnit::from_str(input).unwrap();
1649 {
1650 let mut section = unit.sections().next().unwrap();
1651 section.add("Wants", "foo.service");
1652 section.add("Wants", "bar.service");
1653 }
1654
1655 let section = unit.sections().next().unwrap();
1656 let all_wants = section.get_all("Wants");
1657 assert_eq!(all_wants.len(), 2);
1658 assert_eq!(all_wants[0], "foo.service");
1659 assert_eq!(all_wants[1], "bar.service");
1660 }
1661
1662 #[test]
1663 fn test_remove_entry() {
1664 let input = r#"[Unit]
1665Description=Test Service
1666After=network.target
1667"#;
1668 let unit = SystemdUnit::from_str(input).unwrap();
1669 {
1670 let mut section = unit.sections().next().unwrap();
1671 section.remove("After");
1672 }
1673
1674 let section = unit.sections().next().unwrap();
1675 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1676 assert_eq!(section.get("After"), None);
1677 }
1678
1679 #[test]
1680 fn test_remove_all_entries() {
1681 let input = r#"[Unit]
1682Wants=foo.service
1683Wants=bar.service
1684Description=Test
1685"#;
1686 let unit = SystemdUnit::from_str(input).unwrap();
1687 {
1688 let mut section = unit.sections().next().unwrap();
1689 section.remove_all("Wants");
1690 }
1691
1692 let section = unit.sections().next().unwrap();
1693 assert_eq!(section.get_all("Wants").len(), 0);
1694 assert_eq!(section.get("Description"), Some("Test".to_string()));
1695 }
1696
1697 #[test]
1698 fn test_unescape_basic() {
1699 let input = r#"[Unit]
1700Description=Test\nService
1701"#;
1702 let unit = SystemdUnit::from_str(input).unwrap();
1703 let section = unit.sections().next().unwrap();
1704 let entry = section.entries().next().unwrap();
1705
1706 assert_eq!(entry.value(), Some("Test\\nService".to_string()));
1707 assert_eq!(entry.unescape_value(), Some("Test\nService".to_string()));
1708 }
1709
1710 #[test]
1711 fn test_unescape_all_escapes() {
1712 let input = r#"[Unit]
1713Value=\n\t\r\\\"\'\x41\101\u0041\U00000041
1714"#;
1715 let unit = SystemdUnit::from_str(input).unwrap();
1716 let section = unit.sections().next().unwrap();
1717 let entry = section.entries().next().unwrap();
1718
1719 let unescaped = entry.unescape_value().unwrap();
1720 assert_eq!(unescaped, "\n\t\r\\\"'AAAA");
1724 }
1725
1726 #[test]
1727 fn test_unescape_unicode() {
1728 let input = r#"[Unit]
1729Value=Hello\u0020World\U0001F44D
1730"#;
1731 let unit = SystemdUnit::from_str(input).unwrap();
1732 let section = unit.sections().next().unwrap();
1733 let entry = section.entries().next().unwrap();
1734
1735 let unescaped = entry.unescape_value().unwrap();
1736 assert_eq!(unescaped, "Hello World👍");
1738 }
1739
1740 #[test]
1741 fn test_escape_value() {
1742 let text = "Hello\nWorld\t\"Test\"\\Path";
1743 let escaped = Entry::escape_value(text);
1744 assert_eq!(escaped, "Hello\\nWorld\\t\\\"Test\\\"\\\\Path");
1745 }
1746
1747 #[test]
1748 fn test_escape_unescape_roundtrip() {
1749 let original = "Test\nwith\ttabs\rand\"quotes\"\\backslash";
1750 let escaped = Entry::escape_value(original);
1751 let unescaped = unescape_string(&escaped);
1752 assert_eq!(original, unescaped);
1753 }
1754
1755 #[test]
1756 fn test_unescape_invalid_sequences() {
1757 let input = r#"[Unit]
1759Value=\z\xFF\u12\U1234
1760"#;
1761 let unit = SystemdUnit::from_str(input).unwrap();
1762 let section = unit.sections().next().unwrap();
1763 let entry = section.entries().next().unwrap();
1764
1765 let unescaped = entry.unescape_value().unwrap();
1766 assert!(unescaped.contains("\\z"));
1768 }
1769
1770 #[test]
1771 fn test_quoted_double_quotes() {
1772 let input = r#"[Unit]
1773Description="Test Service"
1774"#;
1775 let unit = SystemdUnit::from_str(input).unwrap();
1776 let section = unit.sections().next().unwrap();
1777 let entry = section.entries().next().unwrap();
1778
1779 assert_eq!(entry.value(), Some("\"Test Service\"".to_string()));
1780 assert_eq!(entry.quoted_value(), Some("\"Test Service\"".to_string()));
1781 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1782 assert_eq!(entry.is_quoted(), Some('"'));
1783 }
1784
1785 #[test]
1786 fn test_quoted_single_quotes() {
1787 let input = r#"[Unit]
1788Description='Test Service'
1789"#;
1790 let unit = SystemdUnit::from_str(input).unwrap();
1791 let section = unit.sections().next().unwrap();
1792 let entry = section.entries().next().unwrap();
1793
1794 assert_eq!(entry.value(), Some("'Test Service'".to_string()));
1795 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1796 assert_eq!(entry.is_quoted(), Some('\''));
1797 }
1798
1799 #[test]
1800 fn test_quoted_with_whitespace() {
1801 let input = r#"[Unit]
1802Description=" Test Service "
1803"#;
1804 let unit = SystemdUnit::from_str(input).unwrap();
1805 let section = unit.sections().next().unwrap();
1806 let entry = section.entries().next().unwrap();
1807
1808 assert_eq!(entry.unquoted_value(), Some(" Test Service ".to_string()));
1810 }
1811
1812 #[test]
1813 fn test_unquoted_value() {
1814 let input = r#"[Unit]
1815Description=Test Service
1816"#;
1817 let unit = SystemdUnit::from_str(input).unwrap();
1818 let section = unit.sections().next().unwrap();
1819 let entry = section.entries().next().unwrap();
1820
1821 assert_eq!(entry.value(), Some("Test Service".to_string()));
1822 assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1823 assert_eq!(entry.is_quoted(), None);
1824 }
1825
1826 #[test]
1827 fn test_mismatched_quotes() {
1828 let input = r#"[Unit]
1829Description="Test Service'
1830"#;
1831 let unit = SystemdUnit::from_str(input).unwrap();
1832 let section = unit.sections().next().unwrap();
1833 let entry = section.entries().next().unwrap();
1834
1835 assert_eq!(entry.is_quoted(), None);
1837 assert_eq!(entry.unquoted_value(), Some("\"Test Service'".to_string()));
1838 }
1839
1840 #[test]
1841 fn test_empty_quotes() {
1842 let input = r#"[Unit]
1843Description=""
1844"#;
1845 let unit = SystemdUnit::from_str(input).unwrap();
1846 let section = unit.sections().next().unwrap();
1847 let entry = section.entries().next().unwrap();
1848
1849 assert_eq!(entry.is_quoted(), Some('"'));
1850 assert_eq!(entry.unquoted_value(), Some("".to_string()));
1851 }
1852
1853 #[test]
1854 fn test_value_as_list() {
1855 let input = r#"[Unit]
1856After=network.target remote-fs.target
1857"#;
1858 let unit = SystemdUnit::from_str(input).unwrap();
1859 let section = unit.sections().next().unwrap();
1860 let entry = section.entries().next().unwrap();
1861
1862 let list = entry.value_as_list();
1863 assert_eq!(list.len(), 2);
1864 assert_eq!(list[0], "network.target");
1865 assert_eq!(list[1], "remote-fs.target");
1866 }
1867
1868 #[test]
1869 fn test_value_as_list_single() {
1870 let input = r#"[Unit]
1871After=network.target
1872"#;
1873 let unit = SystemdUnit::from_str(input).unwrap();
1874 let section = unit.sections().next().unwrap();
1875 let entry = section.entries().next().unwrap();
1876
1877 let list = entry.value_as_list();
1878 assert_eq!(list.len(), 1);
1879 assert_eq!(list[0], "network.target");
1880 }
1881
1882 #[test]
1883 fn test_value_as_list_empty() {
1884 let input = r#"[Unit]
1885After=
1886"#;
1887 let unit = SystemdUnit::from_str(input).unwrap();
1888 let section = unit.sections().next().unwrap();
1889 let entry = section.entries().next().unwrap();
1890
1891 let list = entry.value_as_list();
1892 assert_eq!(list.len(), 0);
1893 }
1894
1895 #[test]
1896 fn test_value_as_list_with_extra_whitespace() {
1897 let input = r#"[Unit]
1898After= network.target remote-fs.target
1899"#;
1900 let unit = SystemdUnit::from_str(input).unwrap();
1901 let section = unit.sections().next().unwrap();
1902 let entry = section.entries().next().unwrap();
1903
1904 let list = entry.value_as_list();
1905 assert_eq!(list.len(), 2);
1906 assert_eq!(list[0], "network.target");
1907 assert_eq!(list[1], "remote-fs.target");
1908 }
1909
1910 #[test]
1911 fn test_section_get_list() {
1912 let input = r#"[Unit]
1913After=network.target remote-fs.target
1914"#;
1915 let unit = SystemdUnit::from_str(input).unwrap();
1916 let section = unit.sections().next().unwrap();
1917
1918 let list = section.get_list("After");
1919 assert_eq!(list.len(), 2);
1920 assert_eq!(list[0], "network.target");
1921 assert_eq!(list[1], "remote-fs.target");
1922 }
1923
1924 #[test]
1925 fn test_section_get_list_missing() {
1926 let input = r#"[Unit]
1927Description=Test
1928"#;
1929 let unit = SystemdUnit::from_str(input).unwrap();
1930 let section = unit.sections().next().unwrap();
1931
1932 let list = section.get_list("After");
1933 assert_eq!(list.len(), 0);
1934 }
1935
1936 #[test]
1937 fn test_section_set_list() {
1938 let input = r#"[Unit]
1939Description=Test
1940"#;
1941 let unit = SystemdUnit::from_str(input).unwrap();
1942 {
1943 let mut section = unit.sections().next().unwrap();
1944 section.set_list("After", &["network.target", "remote-fs.target"]);
1945 }
1946
1947 let section = unit.sections().next().unwrap();
1948 let list = section.get_list("After");
1949 assert_eq!(list.len(), 2);
1950 assert_eq!(list[0], "network.target");
1951 assert_eq!(list[1], "remote-fs.target");
1952 }
1953
1954 #[test]
1955 fn test_section_set_list_replaces() {
1956 let input = r#"[Unit]
1957After=foo.target
1958"#;
1959 let unit = SystemdUnit::from_str(input).unwrap();
1960 {
1961 let mut section = unit.sections().next().unwrap();
1962 section.set_list("After", &["network.target", "remote-fs.target"]);
1963 }
1964
1965 let section = unit.sections().next().unwrap();
1966 let list = section.get_list("After");
1967 assert_eq!(list.len(), 2);
1968 assert_eq!(list[0], "network.target");
1969 assert_eq!(list[1], "remote-fs.target");
1970 }
1971
1972 #[test]
1973 fn test_value_as_bool_positive() {
1974 let inputs = vec!["yes", "true", "1", "on", "YES", "True", "ON"];
1975
1976 for input_val in inputs {
1977 let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1978 let unit = SystemdUnit::from_str(&input).unwrap();
1979 let section = unit.sections().next().unwrap();
1980 let entry = section.entries().next().unwrap();
1981 assert_eq!(
1982 entry.value_as_bool(),
1983 Some(true),
1984 "Failed for input: {}",
1985 input_val
1986 );
1987 }
1988 }
1989
1990 #[test]
1991 fn test_value_as_bool_negative() {
1992 let inputs = vec!["no", "false", "0", "off", "NO", "False", "OFF"];
1993
1994 for input_val in inputs {
1995 let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1996 let unit = SystemdUnit::from_str(&input).unwrap();
1997 let section = unit.sections().next().unwrap();
1998 let entry = section.entries().next().unwrap();
1999 assert_eq!(
2000 entry.value_as_bool(),
2001 Some(false),
2002 "Failed for input: {}",
2003 input_val
2004 );
2005 }
2006 }
2007
2008 #[test]
2009 fn test_value_as_bool_invalid() {
2010 let input = r#"[Service]
2011RemainAfterExit=maybe
2012"#;
2013 let unit = SystemdUnit::from_str(input).unwrap();
2014 let section = unit.sections().next().unwrap();
2015 let entry = section.entries().next().unwrap();
2016 assert_eq!(entry.value_as_bool(), None);
2017 }
2018
2019 #[test]
2020 fn test_value_as_bool_with_whitespace() {
2021 let input = r#"[Service]
2022RemainAfterExit= yes
2023"#;
2024 let unit = SystemdUnit::from_str(input).unwrap();
2025 let section = unit.sections().next().unwrap();
2026 let entry = section.entries().next().unwrap();
2027 assert_eq!(entry.value_as_bool(), Some(true));
2028 }
2029
2030 #[test]
2031 fn test_format_bool() {
2032 assert_eq!(Entry::format_bool(true), "yes");
2033 assert_eq!(Entry::format_bool(false), "no");
2034 }
2035
2036 #[test]
2037 fn test_section_get_bool() {
2038 let input = r#"[Service]
2039RemainAfterExit=yes
2040Type=simple
2041"#;
2042 let unit = SystemdUnit::from_str(input).unwrap();
2043 let section = unit.sections().next().unwrap();
2044
2045 assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
2046 assert_eq!(section.get_bool("Type"), None); assert_eq!(section.get_bool("Missing"), None); }
2049
2050 #[test]
2051 fn test_section_set_bool() {
2052 let input = r#"[Service]
2053Type=simple
2054"#;
2055 let unit = SystemdUnit::from_str(input).unwrap();
2056 {
2057 let mut section = unit.sections().next().unwrap();
2058 section.set_bool("RemainAfterExit", true);
2059 section.set_bool("PrivateTmp", false);
2060 }
2061
2062 let section = unit.sections().next().unwrap();
2063 assert_eq!(section.get("RemainAfterExit"), Some("yes".to_string()));
2064 assert_eq!(section.get("PrivateTmp"), Some("no".to_string()));
2065 assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
2066 assert_eq!(section.get_bool("PrivateTmp"), Some(false));
2067 }
2068
2069 #[test]
2070 fn test_add_entry_with_trailing_whitespace() {
2071 let input = r#"[Unit]
2073Description=Test Service
2074
2075"#;
2076 let unit = SystemdUnit::from_str(input).unwrap();
2077 {
2078 let mut section = unit.sections().next().unwrap();
2079 section.add("After", "network.target");
2080 }
2081
2082 let output = unit.text();
2083 let expected = r#"[Unit]
2085Description=Test Service
2086After=network.target
2087
2088"#;
2089 assert_eq!(output, expected);
2090 }
2091
2092 #[test]
2093 fn test_set_new_entry_with_trailing_whitespace() {
2094 let input = r#"[Unit]
2096Description=Test Service
2097
2098"#;
2099 let unit = SystemdUnit::from_str(input).unwrap();
2100 {
2101 let mut section = unit.sections().next().unwrap();
2102 section.set("After", "network.target");
2103 }
2104
2105 let output = unit.text();
2106 let expected = r#"[Unit]
2108Description=Test Service
2109After=network.target
2110
2111"#;
2112 assert_eq!(output, expected);
2113 }
2114
2115 #[test]
2116 fn test_remove_value_from_space_separated_list() {
2117 let input = r#"[Unit]
2118After=network.target syslog.target remote-fs.target
2119"#;
2120 let unit = SystemdUnit::from_str(input).unwrap();
2121 {
2122 let mut section = unit.sections().next().unwrap();
2123 section.remove_value("After", "syslog.target");
2124 }
2125
2126 let section = unit.sections().next().unwrap();
2127 assert_eq!(
2128 section.get("After"),
2129 Some("network.target remote-fs.target".to_string())
2130 );
2131 }
2132
2133 #[test]
2134 fn test_remove_value_removes_entire_entry() {
2135 let input = r#"[Unit]
2136After=syslog.target
2137Description=Test
2138"#;
2139 let unit = SystemdUnit::from_str(input).unwrap();
2140 {
2141 let mut section = unit.sections().next().unwrap();
2142 section.remove_value("After", "syslog.target");
2143 }
2144
2145 let section = unit.sections().next().unwrap();
2146 assert_eq!(section.get("After"), None);
2147 assert_eq!(section.get("Description"), Some("Test".to_string()));
2148 }
2149
2150 #[test]
2151 fn test_remove_value_from_multiple_entries() {
2152 let input = r#"[Unit]
2153After=network.target syslog.target
2154After=remote-fs.target
2155After=syslog.target multi-user.target
2156"#;
2157 let unit = SystemdUnit::from_str(input).unwrap();
2158 {
2159 let mut section = unit.sections().next().unwrap();
2160 section.remove_value("After", "syslog.target");
2161 }
2162
2163 let section = unit.sections().next().unwrap();
2164 let all_after = section.get_all("After");
2165 assert_eq!(all_after.len(), 3);
2166 assert_eq!(all_after[0], "network.target");
2167 assert_eq!(all_after[1], "remote-fs.target");
2168 assert_eq!(all_after[2], "multi-user.target");
2169 }
2170
2171 #[test]
2172 fn test_remove_value_not_found() {
2173 let input = r#"[Unit]
2174After=network.target remote-fs.target
2175"#;
2176 let unit = SystemdUnit::from_str(input).unwrap();
2177 {
2178 let mut section = unit.sections().next().unwrap();
2179 section.remove_value("After", "nonexistent.target");
2180 }
2181
2182 let section = unit.sections().next().unwrap();
2183 assert_eq!(
2185 section.get("After"),
2186 Some("network.target remote-fs.target".to_string())
2187 );
2188 }
2189
2190 #[test]
2191 fn test_remove_value_preserves_order() {
2192 let input = r#"[Unit]
2193Description=Test Service
2194After=network.target syslog.target
2195Wants=foo.service
2196After=remote-fs.target
2197Requires=bar.service
2198"#;
2199 let unit = SystemdUnit::from_str(input).unwrap();
2200 {
2201 let mut section = unit.sections().next().unwrap();
2202 section.remove_value("After", "syslog.target");
2203 }
2204
2205 let section = unit.sections().next().unwrap();
2206 let entries: Vec<_> = section.entries().collect();
2207
2208 assert_eq!(entries[0].key(), Some("Description".to_string()));
2210 assert_eq!(entries[1].key(), Some("After".to_string()));
2211 assert_eq!(entries[1].value(), Some("network.target".to_string()));
2212 assert_eq!(entries[2].key(), Some("Wants".to_string()));
2213 assert_eq!(entries[3].key(), Some("After".to_string()));
2214 assert_eq!(entries[3].value(), Some("remote-fs.target".to_string()));
2215 assert_eq!(entries[4].key(), Some("Requires".to_string()));
2216 }
2217
2218 #[test]
2219 fn test_remove_value_key_not_found() {
2220 let input = r#"[Unit]
2221Description=Test Service
2222"#;
2223 let unit = SystemdUnit::from_str(input).unwrap();
2224 {
2225 let mut section = unit.sections().next().unwrap();
2226 section.remove_value("After", "network.target");
2227 }
2228
2229 let section = unit.sections().next().unwrap();
2231 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
2232 assert_eq!(section.get("After"), None);
2233 }
2234
2235 #[test]
2236 fn test_entry_set_value_basic() {
2237 let input = r#"[Unit]
2238After=network.target
2239Description=Test
2240"#;
2241 let unit = SystemdUnit::from_str(input).unwrap();
2242 let section = unit.get_section("Unit").unwrap();
2243
2244 for entry in section.entries() {
2245 if entry.key().as_deref() == Some("After") {
2246 entry.set_value("remote-fs.target");
2247 }
2248 }
2249
2250 let section = unit.get_section("Unit").unwrap();
2251 assert_eq!(section.get("After"), Some("remote-fs.target".to_string()));
2252 assert_eq!(section.get("Description"), Some("Test".to_string()));
2253 }
2254
2255 #[test]
2256 fn test_entry_set_value_preserves_order() {
2257 let input = r#"[Unit]
2258Description=Test Service
2259After=network.target syslog.target
2260Wants=foo.service
2261After=remote-fs.target
2262Requires=bar.service
2263"#;
2264 let unit = SystemdUnit::from_str(input).unwrap();
2265 let section = unit.get_section("Unit").unwrap();
2266
2267 for entry in section.entries() {
2268 if entry.key().as_deref() == Some("After") {
2269 let values = entry.value_as_list();
2270 let filtered: Vec<_> = values
2271 .iter()
2272 .filter(|v| v.as_str() != "syslog.target")
2273 .map(|s| s.as_str())
2274 .collect();
2275 if !filtered.is_empty() {
2276 entry.set_value(&filtered.join(" "));
2277 }
2278 }
2279 }
2280
2281 let section = unit.get_section("Unit").unwrap();
2282 let entries: Vec<_> = section.entries().collect();
2283
2284 assert_eq!(entries[0].key(), Some("Description".to_string()));
2286 assert_eq!(entries[1].key(), Some("After".to_string()));
2287 assert_eq!(entries[1].value(), Some("network.target".to_string()));
2288 assert_eq!(entries[2].key(), Some("Wants".to_string()));
2289 assert_eq!(entries[3].key(), Some("After".to_string()));
2290 assert_eq!(entries[3].value(), Some("remote-fs.target".to_string()));
2291 assert_eq!(entries[4].key(), Some("Requires".to_string()));
2292 }
2293
2294 #[test]
2295 fn test_entry_set_value_multiple_entries() {
2296 let input = r#"[Unit]
2297After=network.target
2298After=syslog.target
2299After=remote-fs.target
2300"#;
2301 let unit = SystemdUnit::from_str(input).unwrap();
2302 let section = unit.get_section("Unit").unwrap();
2303
2304 let entries_to_modify: Vec<_> = section
2306 .entries()
2307 .filter(|e| e.key().as_deref() == Some("After"))
2308 .collect();
2309
2310 for entry in entries_to_modify {
2312 let old_value = entry.value().unwrap();
2313 entry.set_value(&format!("{} multi-user.target", old_value));
2314 }
2315
2316 let section = unit.get_section("Unit").unwrap();
2317 let all_after = section.get_all("After");
2318 assert_eq!(all_after.len(), 3);
2319 assert_eq!(all_after[0], "network.target multi-user.target");
2320 assert_eq!(all_after[1], "syslog.target multi-user.target");
2321 assert_eq!(all_after[2], "remote-fs.target multi-user.target");
2322 }
2323
2324 #[test]
2325 fn test_entry_set_value_with_empty_string() {
2326 let input = r#"[Unit]
2327After=network.target
2328"#;
2329 let unit = SystemdUnit::from_str(input).unwrap();
2330 let section = unit.get_section("Unit").unwrap();
2331
2332 for entry in section.entries() {
2333 if entry.key().as_deref() == Some("After") {
2334 entry.set_value("");
2335 }
2336 }
2337
2338 let section = unit.get_section("Unit").unwrap();
2339 assert_eq!(section.get("After"), Some("".to_string()));
2340 }
2341
2342 #[test]
2343 fn test_remove_entries_where_basic() {
2344 let input = r#"[Unit]
2345After=network.target
2346Wants=foo.service
2347After=syslog.target
2348"#;
2349 let unit = SystemdUnit::from_str(input).unwrap();
2350 {
2351 let mut section = unit.sections().next().unwrap();
2352 section.remove_entries_where(|key, _value| key == "After");
2353 }
2354
2355 let section = unit.sections().next().unwrap();
2356 assert_eq!(section.get_all("After").len(), 0);
2357 assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
2358 }
2359
2360 #[test]
2361 fn test_remove_entries_where_with_value_check() {
2362 let input = r#"[Unit]
2363After=network.target syslog.target
2364Wants=foo.service
2365After=remote-fs.target
2366"#;
2367 let unit = SystemdUnit::from_str(input).unwrap();
2368 {
2369 let mut section = unit.sections().next().unwrap();
2370 section.remove_entries_where(|key, value| {
2371 key == "After" && value.split_whitespace().any(|v| v == "syslog.target")
2372 });
2373 }
2374
2375 let section = unit.sections().next().unwrap();
2376 let all_after = section.get_all("After");
2377 assert_eq!(all_after.len(), 1);
2378 assert_eq!(all_after[0], "remote-fs.target");
2379 assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
2380 }
2381
2382 #[test]
2383 fn test_remove_entries_where_preserves_order() {
2384 let input = r#"[Unit]
2385Description=Test Service
2386After=network.target
2387Wants=foo.service
2388After=syslog.target
2389Requires=bar.service
2390After=remote-fs.target
2391"#;
2392 let unit = SystemdUnit::from_str(input).unwrap();
2393 {
2394 let mut section = unit.sections().next().unwrap();
2395 section.remove_entries_where(|key, value| key == "After" && value.contains("syslog"));
2396 }
2397
2398 let section = unit.sections().next().unwrap();
2399 let entries: Vec<_> = section.entries().collect();
2400
2401 assert_eq!(entries.len(), 5);
2402 assert_eq!(entries[0].key(), Some("Description".to_string()));
2403 assert_eq!(entries[1].key(), Some("After".to_string()));
2404 assert_eq!(entries[1].value(), Some("network.target".to_string()));
2405 assert_eq!(entries[2].key(), Some("Wants".to_string()));
2406 assert_eq!(entries[3].key(), Some("Requires".to_string()));
2407 assert_eq!(entries[4].key(), Some("After".to_string()));
2408 assert_eq!(entries[4].value(), Some("remote-fs.target".to_string()));
2409 }
2410
2411 #[test]
2412 fn test_remove_entries_where_no_matches() {
2413 let input = r#"[Unit]
2414After=network.target
2415Wants=foo.service
2416"#;
2417 let unit = SystemdUnit::from_str(input).unwrap();
2418 {
2419 let mut section = unit.sections().next().unwrap();
2420 section.remove_entries_where(|key, _value| key == "Requires");
2421 }
2422
2423 let section = unit.sections().next().unwrap();
2424 assert_eq!(section.get("After"), Some("network.target".to_string()));
2425 assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
2426 }
2427
2428 #[test]
2429 fn test_remove_entries_where_all_entries() {
2430 let input = r#"[Unit]
2431After=network.target
2432Wants=foo.service
2433Requires=bar.service
2434"#;
2435 let unit = SystemdUnit::from_str(input).unwrap();
2436 {
2437 let mut section = unit.sections().next().unwrap();
2438 section.remove_entries_where(|_key, _value| true);
2439 }
2440
2441 let section = unit.sections().next().unwrap();
2442 assert_eq!(section.entries().count(), 0);
2443 }
2444
2445 #[test]
2446 fn test_remove_entries_where_complex_predicate() {
2447 let input = r#"[Unit]
2448After=network.target
2449After=syslog.target remote-fs.target
2450Wants=foo.service
2451After=multi-user.target
2452Requires=bar.service
2453"#;
2454 let unit = SystemdUnit::from_str(input).unwrap();
2455 {
2456 let mut section = unit.sections().next().unwrap();
2457 section.remove_entries_where(|key, value| {
2459 key == "After" && value.split_whitespace().count() > 1
2460 });
2461 }
2462
2463 let section = unit.sections().next().unwrap();
2464 let all_after = section.get_all("After");
2465 assert_eq!(all_after.len(), 2);
2466 assert_eq!(all_after[0], "network.target");
2467 assert_eq!(all_after[1], "multi-user.target");
2468 }
2469
2470 #[test]
2471 fn test_insert_at_beginning() {
2472 let input = r#"[Unit]
2473Description=Test Service
2474After=network.target
2475"#;
2476 let unit = SystemdUnit::from_str(input).unwrap();
2477 {
2478 let mut section = unit.sections().next().unwrap();
2479 section.insert_at(0, "Wants", "foo.service");
2480 }
2481
2482 let section = unit.sections().next().unwrap();
2483 let entries: Vec<_> = section.entries().collect();
2484 assert_eq!(entries.len(), 3);
2485 assert_eq!(entries[0].key(), Some("Wants".to_string()));
2486 assert_eq!(entries[0].value(), Some("foo.service".to_string()));
2487 assert_eq!(entries[1].key(), Some("Description".to_string()));
2488 assert_eq!(entries[2].key(), Some("After".to_string()));
2489 }
2490
2491 #[test]
2492 fn test_insert_at_middle() {
2493 let input = r#"[Unit]
2494Description=Test Service
2495After=network.target
2496"#;
2497 let unit = SystemdUnit::from_str(input).unwrap();
2498 {
2499 let mut section = unit.sections().next().unwrap();
2500 section.insert_at(1, "Wants", "foo.service");
2501 }
2502
2503 let section = unit.sections().next().unwrap();
2504 let entries: Vec<_> = section.entries().collect();
2505 assert_eq!(entries.len(), 3);
2506 assert_eq!(entries[0].key(), Some("Description".to_string()));
2507 assert_eq!(entries[1].key(), Some("Wants".to_string()));
2508 assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2509 assert_eq!(entries[2].key(), Some("After".to_string()));
2510 }
2511
2512 #[test]
2513 fn test_insert_at_end() {
2514 let input = r#"[Unit]
2515Description=Test Service
2516After=network.target
2517"#;
2518 let unit = SystemdUnit::from_str(input).unwrap();
2519 {
2520 let mut section = unit.sections().next().unwrap();
2521 section.insert_at(2, "Wants", "foo.service");
2522 }
2523
2524 let section = unit.sections().next().unwrap();
2525 let entries: Vec<_> = section.entries().collect();
2526 assert_eq!(entries.len(), 3);
2527 assert_eq!(entries[0].key(), Some("Description".to_string()));
2528 assert_eq!(entries[1].key(), Some("After".to_string()));
2529 assert_eq!(entries[2].key(), Some("Wants".to_string()));
2530 assert_eq!(entries[2].value(), Some("foo.service".to_string()));
2531 }
2532
2533 #[test]
2534 fn test_insert_at_beyond_end() {
2535 let input = r#"[Unit]
2536Description=Test Service
2537"#;
2538 let unit = SystemdUnit::from_str(input).unwrap();
2539 {
2540 let mut section = unit.sections().next().unwrap();
2541 section.insert_at(100, "Wants", "foo.service");
2542 }
2543
2544 let section = unit.sections().next().unwrap();
2545 let entries: Vec<_> = section.entries().collect();
2546 assert_eq!(entries.len(), 2);
2547 assert_eq!(entries[0].key(), Some("Description".to_string()));
2548 assert_eq!(entries[1].key(), Some("Wants".to_string()));
2549 assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2550 }
2551
2552 #[test]
2553 fn test_insert_at_empty_section() {
2554 let input = r#"[Unit]
2555"#;
2556 let unit = SystemdUnit::from_str(input).unwrap();
2557 {
2558 let mut section = unit.sections().next().unwrap();
2559 section.insert_at(0, "Description", "Test Service");
2560 }
2561
2562 let section = unit.sections().next().unwrap();
2563 assert_eq!(section.get("Description"), Some("Test Service".to_string()));
2564 }
2565
2566 #[test]
2567 fn test_insert_before_basic() {
2568 let input = r#"[Unit]
2569Description=Test Service
2570After=network.target
2571"#;
2572 let unit = SystemdUnit::from_str(input).unwrap();
2573 {
2574 let mut section = unit.sections().next().unwrap();
2575 section.insert_before("After", "Wants", "foo.service");
2576 }
2577
2578 let section = unit.sections().next().unwrap();
2579 let entries: Vec<_> = section.entries().collect();
2580 assert_eq!(entries.len(), 3);
2581 assert_eq!(entries[0].key(), Some("Description".to_string()));
2582 assert_eq!(entries[1].key(), Some("Wants".to_string()));
2583 assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2584 assert_eq!(entries[2].key(), Some("After".to_string()));
2585 }
2586
2587 #[test]
2588 fn test_insert_before_first_entry() {
2589 let input = r#"[Unit]
2590Description=Test Service
2591After=network.target
2592"#;
2593 let unit = SystemdUnit::from_str(input).unwrap();
2594 {
2595 let mut section = unit.sections().next().unwrap();
2596 section.insert_before("Description", "Wants", "foo.service");
2597 }
2598
2599 let section = unit.sections().next().unwrap();
2600 let entries: Vec<_> = section.entries().collect();
2601 assert_eq!(entries.len(), 3);
2602 assert_eq!(entries[0].key(), Some("Wants".to_string()));
2603 assert_eq!(entries[0].value(), Some("foo.service".to_string()));
2604 assert_eq!(entries[1].key(), Some("Description".to_string()));
2605 assert_eq!(entries[2].key(), Some("After".to_string()));
2606 }
2607
2608 #[test]
2609 fn test_insert_before_nonexistent_key() {
2610 let input = r#"[Unit]
2611Description=Test Service
2612"#;
2613 let unit = SystemdUnit::from_str(input).unwrap();
2614 {
2615 let mut section = unit.sections().next().unwrap();
2616 section.insert_before("After", "Wants", "foo.service");
2617 }
2618
2619 let section = unit.sections().next().unwrap();
2620 let entries: Vec<_> = section.entries().collect();
2621 assert_eq!(entries.len(), 1);
2622 assert_eq!(entries[0].key(), Some("Description".to_string()));
2623 }
2624
2625 #[test]
2626 fn test_insert_before_multiple_occurrences() {
2627 let input = r#"[Unit]
2628After=network.target
2629After=syslog.target
2630"#;
2631 let unit = SystemdUnit::from_str(input).unwrap();
2632 {
2633 let mut section = unit.sections().next().unwrap();
2634 section.insert_before("After", "Wants", "foo.service");
2635 }
2636
2637 let section = unit.sections().next().unwrap();
2638 let entries: Vec<_> = section.entries().collect();
2639 assert_eq!(entries.len(), 3);
2640 assert_eq!(entries[0].key(), Some("Wants".to_string()));
2641 assert_eq!(entries[1].key(), Some("After".to_string()));
2642 assert_eq!(entries[1].value(), Some("network.target".to_string()));
2643 assert_eq!(entries[2].key(), Some("After".to_string()));
2644 assert_eq!(entries[2].value(), Some("syslog.target".to_string()));
2645 }
2646
2647 #[test]
2648 fn test_insert_after_basic() {
2649 let input = r#"[Unit]
2650Description=Test Service
2651After=network.target
2652"#;
2653 let unit = SystemdUnit::from_str(input).unwrap();
2654 {
2655 let mut section = unit.sections().next().unwrap();
2656 section.insert_after("Description", "Wants", "foo.service");
2657 }
2658
2659 let section = unit.sections().next().unwrap();
2660 let entries: Vec<_> = section.entries().collect();
2661 assert_eq!(entries.len(), 3);
2662 assert_eq!(entries[0].key(), Some("Description".to_string()));
2663 assert_eq!(entries[1].key(), Some("Wants".to_string()));
2664 assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2665 assert_eq!(entries[2].key(), Some("After".to_string()));
2666 }
2667
2668 #[test]
2669 fn test_insert_after_last_entry() {
2670 let input = r#"[Unit]
2671Description=Test Service
2672After=network.target
2673"#;
2674 let unit = SystemdUnit::from_str(input).unwrap();
2675 {
2676 let mut section = unit.sections().next().unwrap();
2677 section.insert_after("After", "Wants", "foo.service");
2678 }
2679
2680 let section = unit.sections().next().unwrap();
2681 let entries: Vec<_> = section.entries().collect();
2682 assert_eq!(entries.len(), 3);
2683 assert_eq!(entries[0].key(), Some("Description".to_string()));
2684 assert_eq!(entries[1].key(), Some("After".to_string()));
2685 assert_eq!(entries[2].key(), Some("Wants".to_string()));
2686 assert_eq!(entries[2].value(), Some("foo.service".to_string()));
2687 }
2688
2689 #[test]
2690 fn test_insert_after_nonexistent_key() {
2691 let input = r#"[Unit]
2692Description=Test Service
2693"#;
2694 let unit = SystemdUnit::from_str(input).unwrap();
2695 {
2696 let mut section = unit.sections().next().unwrap();
2697 section.insert_after("After", "Wants", "foo.service");
2698 }
2699
2700 let section = unit.sections().next().unwrap();
2701 let entries: Vec<_> = section.entries().collect();
2702 assert_eq!(entries.len(), 1);
2703 assert_eq!(entries[0].key(), Some("Description".to_string()));
2704 }
2705
2706 #[test]
2707 fn test_insert_after_multiple_occurrences() {
2708 let input = r#"[Unit]
2709After=network.target
2710After=syslog.target
2711"#;
2712 let unit = SystemdUnit::from_str(input).unwrap();
2713 {
2714 let mut section = unit.sections().next().unwrap();
2715 section.insert_after("After", "Wants", "foo.service");
2716 }
2717
2718 let section = unit.sections().next().unwrap();
2719 let entries: Vec<_> = section.entries().collect();
2720 assert_eq!(entries.len(), 3);
2721 assert_eq!(entries[0].key(), Some("After".to_string()));
2722 assert_eq!(entries[0].value(), Some("network.target".to_string()));
2723 assert_eq!(entries[1].key(), Some("Wants".to_string()));
2724 assert_eq!(entries[2].key(), Some("After".to_string()));
2725 assert_eq!(entries[2].value(), Some("syslog.target".to_string()));
2726 }
2727
2728 #[test]
2729 fn test_insert_preserves_whitespace() {
2730 let input = r#"[Unit]
2731Description=Test Service
2732
2733After=network.target
2734"#;
2735 let unit = SystemdUnit::from_str(input).unwrap();
2736 {
2737 let mut section = unit.sections().next().unwrap();
2738 section.insert_at(1, "Wants", "foo.service");
2739 }
2740
2741 let section = unit.sections().next().unwrap();
2742 let entries: Vec<_> = section.entries().collect();
2743 assert_eq!(entries.len(), 3);
2744 assert_eq!(entries[0].key(), Some("Description".to_string()));
2745 assert_eq!(entries[1].key(), Some("Wants".to_string()));
2746 assert_eq!(entries[2].key(), Some("After".to_string()));
2747
2748 let expected = r#"[Unit]
2749Description=Test Service
2750
2751Wants=foo.service
2752After=network.target
2753"#;
2754 assert_eq!(unit.text(), expected);
2755 }
2756
2757 #[test]
2758 fn test_line_col() {
2759 let text = r#"[Unit]
2760Description=Test Service
2761After=network.target
2762
2763[Service]
2764Type=simple
2765ExecStart=/usr/bin/test
2766Environment="FOO=bar"
2767"#;
2768 let unit = SystemdUnit::from_str(text).unwrap();
2769
2770 assert_eq!(unit.line(), 0);
2772 assert_eq!(unit.column(), 0);
2773 assert_eq!(unit.line_col(), (0, 0));
2774
2775 let sections: Vec<_> = unit.sections().collect();
2777 assert_eq!(sections.len(), 2);
2778
2779 assert_eq!(sections[0].line(), 0);
2781 assert_eq!(sections[0].column(), 0);
2782 assert_eq!(sections[0].line_col(), (0, 0));
2783
2784 assert_eq!(sections[1].line(), 4);
2786 assert_eq!(sections[1].column(), 0);
2787 assert_eq!(sections[1].line_col(), (4, 0));
2788
2789 let unit_entries: Vec<_> = sections[0].entries().collect();
2791 assert_eq!(unit_entries.len(), 2);
2792 assert_eq!(unit_entries[0].line(), 1); assert_eq!(unit_entries[0].column(), 0); assert_eq!(unit_entries[1].line(), 2); assert_eq!(unit_entries[1].column(), 0); let service_entries: Vec<_> = sections[1].entries().collect();
2798 assert_eq!(service_entries.len(), 3);
2799 assert_eq!(service_entries[0].line(), 5); assert_eq!(service_entries[0].column(), 0); assert_eq!(service_entries[1].line(), 6); assert_eq!(service_entries[1].column(), 0); assert_eq!(service_entries[2].line(), 7); assert_eq!(service_entries[2].column(), 0); assert_eq!(unit_entries[0].line_col(), (1, 0));
2808 assert_eq!(service_entries[2].line_col(), (7, 0));
2809 }
2810
2811 #[test]
2812 fn test_line_col_multiline() {
2813 let text = r#"[Unit]
2815Description=A long \
2816value that spans \
2817multiple lines
2818After=network.target
2819"#;
2820 let unit = SystemdUnit::from_str(text).unwrap();
2821 let section = unit.sections().next().unwrap();
2822 let entries: Vec<_> = section.entries().collect();
2823
2824 assert_eq!(entries.len(), 2);
2825 assert_eq!(entries[0].line(), 1);
2827 assert_eq!(entries[0].column(), 0);
2828
2829 assert_eq!(entries[1].line(), 4);
2831 assert_eq!(entries[1].column(), 0);
2832 }
2833
2834 #[test]
2835 fn test_leading_whitespace_error() {
2836 let input = r#"[Unit]
2838Description=Test Service
2839 ConditionVirtualization=microsoft
2840"#;
2841 let result = SystemdUnit::from_str(input);
2842
2843 assert!(
2845 result.is_err(),
2846 "Expected parse error for leading whitespace"
2847 );
2848
2849 match result {
2850 Err(Error::ParseError(err)) => {
2851 assert!(
2852 err.0
2853 .iter()
2854 .any(|e| e.contains("unexpected whitespace at start of line")),
2855 "Expected error about leading whitespace, got: {:?}",
2856 err.0
2857 );
2858 }
2859 _ => panic!("Expected ParseError, got: {:?}", result),
2860 }
2861 }
2862
2863 #[test]
2864 fn test_leading_whitespace_does_not_hang() {
2865 let input = r#"[Unit]
2867Description=Test Service
2868 After=network.target
2869Wants=foo.service
2870"#;
2871 let result = SystemdUnit::from_str(input);
2873 assert!(
2874 result.is_err(),
2875 "Expected parse error for leading whitespace"
2876 );
2877 }
2878
2879 #[test]
2880 fn test_leading_whitespace_multiple_lines() {
2881 let input = r#"[Unit]
2883Description=Test Service
2884 After=network.target
2885 Wants=foo.service
2886 Requires=bar.service
2887"#;
2888 let result = SystemdUnit::from_str(input);
2889 assert!(
2890 result.is_err(),
2891 "Expected parse error for leading whitespace"
2892 );
2893
2894 match result {
2895 Err(Error::ParseError(err)) => {
2896 assert!(
2898 err.0.len() >= 3,
2899 "Expected at least 3 errors for 3 lines with leading whitespace, got {}",
2900 err.0.len()
2901 );
2902 }
2903 _ => panic!("Expected ParseError"),
2904 }
2905 }
2906
2907 #[test]
2908 fn test_valid_continuation_line() {
2909 let input = r#"[Service]
2911ExecStart=/bin/echo \
2912 hello world
2913"#;
2914 let unit = SystemdUnit::from_str(input).unwrap();
2915
2916 let section = unit.sections().next().unwrap();
2918 let entry = section.entries().next().unwrap();
2919 assert_eq!(entry.key(), Some("ExecStart".to_string()));
2920 }
2921
2922 #[test]
2923 fn test_blank_lines_with_whitespace() {
2924 let input = "[Unit]\nDescription=Test\n \t \nAfter=network.target\n";
2926 let unit = SystemdUnit::from_str(input).unwrap();
2927
2928 let section = unit.sections().next().unwrap();
2930 assert_eq!(section.get("Description"), Some("Test".to_string()));
2931 assert_eq!(section.get("After"), Some("network.target".to_string()));
2932 }
2933}