systemd_unit_edit/
unit.rs

1//! Parser for systemd unit files.
2//!
3//! This parser can be used to parse systemd unit files (as specified
4//! by the [systemd.syntax(7)](https://www.freedesktop.org/software/systemd/man/latest/systemd.syntax.html)),
5//! while preserving all whitespace and comments. It is based on
6//! the [rowan] library, which is a lossless parser library for Rust.
7//!
8//! Once parsed, the file can be traversed or modified, and then written back to a file.
9//!
10//! # Example
11//!
12//! ```
13//! use systemd_unit_edit::SystemdUnit;
14//! use std::str::FromStr;
15//!
16//! # let input = r#"[Unit]
17//! # Description=Test Service
18//! # After=network.target
19//! #
20//! # [Service]
21//! # Type=simple
22//! # ExecStart=/usr/bin/test
23//! # "#;
24//! # let unit = SystemdUnit::from_str(input).unwrap();
25//! # assert_eq!(unit.sections().count(), 2);
26//! # let section = unit.sections().next().unwrap();
27//! # assert_eq!(section.name(), Some("Unit".to_string()));
28//! ```
29
30use crate::lex::{lex, SyntaxKind};
31use rowan::ast::AstNode;
32use rowan::{GreenNode, GreenNodeBuilder};
33use std::path::Path;
34use std::str::FromStr;
35
36/// A positioned parse error containing location information.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct PositionedParseError {
39    /// The error message
40    pub message: String,
41    /// The text range where the error occurred
42    pub range: rowan::TextRange,
43    /// Optional error code for categorization
44    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/// List of encountered syntax errors.
56#[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/// Error parsing systemd unit files
71#[derive(Debug)]
72pub enum Error {
73    /// A syntax error was encountered while parsing the file.
74    ParseError(ParseError),
75
76    /// An I/O error was encountered while reading the file.
77    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/// Language definition for rowan
104#[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
119/// Internal parse result
120pub(crate) struct ParseResult {
121    pub(crate) green_node: GreenNode,
122    pub(crate) errors: Vec<String>,
123    pub(crate) positioned_errors: Vec<PositionedParseError>,
124}
125
126/// Parse a systemd unit file
127pub(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                        // Check if followed by newline
169                        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(); // whitespace
175                            self.bump(); // newline
176                            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            // Consume '['
190            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            // Consume section name
198            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            // Consume ']'
206            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            // Consume newline if present
214            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            // Handle comment before entry
225            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            // Parse key
235            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            // Parse '='
245            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            // Parse value (may include line continuations)
254            while let Some(kind) = self.current() {
255                match kind {
256                    SyntaxKind::VALUE => self.bump(),
257                    SyntaxKind::LINE_CONTINUATION => {
258                        self.bump();
259                        // After line continuation, skip leading whitespace
260                        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            // Parse section header
277            self.parse_section_header();
278
279            // Parse entries until we hit another section header or EOF
280            while let Some(kind) = self.current() {
281                match kind {
282                    SyntaxKind::LEFT_BRACKET => break, // Start of next section
283                    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            // Skip leading blank lines and comments
302            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            // Parse sections
320            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
354// Type aliases for convenience
355type SyntaxNode = rowan::SyntaxNode<Lang>;
356
357/// The root of a systemd unit file
358#[derive(Debug, Clone, PartialEq, Eq, Hash)]
359pub struct SystemdUnit(SyntaxNode);
360
361impl SystemdUnit {
362    /// Get all sections in the file
363    pub fn sections(&self) -> impl Iterator<Item = Section> {
364        self.0.children().filter_map(Section::cast)
365    }
366
367    /// Get a specific section by name
368    pub fn get_section(&self, name: &str) -> Option<Section> {
369        self.sections().find(|s| s.name().as_deref() == Some(name))
370    }
371
372    /// Add a new section to the unit file
373    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    /// Get the raw syntax node
381    pub fn syntax(&self) -> &SyntaxNode {
382        &self.0
383    }
384
385    /// Convert to a string (same as Display::fmt)
386    pub fn text(&self) -> String {
387        self.0.text().to_string()
388    }
389
390    /// Load from a file
391    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    /// Write to a file
397    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/// A section in a systemd unit file (e.g., [Unit], [Service])
443#[derive(Debug, Clone, PartialEq, Eq, Hash)]
444pub struct Section(SyntaxNode);
445
446impl Section {
447    /// Create a new section with the given name
448    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        // Build section header
455        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    /// Get the name of the section
467    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    /// Get all entries in the section
479    pub fn entries(&self) -> impl Iterator<Item = Entry> {
480        self.0.children().filter_map(Entry::cast)
481    }
482
483    /// Get a specific entry by key
484    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    /// Get all values for a key (systemd allows multiple entries with the same key)
491    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    /// Set a value for a key (replaces the first occurrence or adds if it doesn't exist)
499    pub fn set(&mut self, key: &str, value: &str) {
500        let new_entry = Entry::new(key, value);
501
502        // Check if the field already exists and replace the first occurrence
503        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        // Field doesn't exist, append at the end (before trailing whitespace)
514        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    /// Add a value for a key (appends even if the key already exists)
532    pub fn add(&mut self, key: &str, value: &str) {
533        let new_entry = Entry::new(key, value);
534
535        // Find the last non-whitespace child to insert after
536        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    /// Insert a value at a specific position (index is among entries only, not all nodes)
554    ///
555    /// If the index is greater than or equal to the number of entries, the entry
556    /// will be appended at the end.
557    ///
558    /// # Example
559    ///
560    /// ```
561    /// # use systemd_unit_edit::SystemdUnit;
562    /// # use std::str::FromStr;
563    /// let input = r#"[Unit]
564    /// Description=Test Service
565    /// After=network.target
566    /// "#;
567    /// let unit = SystemdUnit::from_str(input).unwrap();
568    /// {
569    ///     let mut section = unit.get_section("Unit").unwrap();
570    ///     section.insert_at(1, "Wants", "foo.service");
571    /// }
572    ///
573    /// let section = unit.get_section("Unit").unwrap();
574    /// let entries: Vec<_> = section.entries().collect();
575    /// assert_eq!(entries[0].key(), Some("Description".to_string()));
576    /// assert_eq!(entries[1].key(), Some("Wants".to_string()));
577    /// assert_eq!(entries[2].key(), Some("After".to_string()));
578    /// ```
579    pub fn insert_at(&mut self, index: usize, key: &str, value: &str) {
580        let new_entry = Entry::new(key, value);
581
582        // Find the insertion point by counting entries
583        let entries: Vec<_> = self.entries().collect();
584
585        if index >= entries.len() {
586            // If index is beyond the end, just append
587            self.add(key, value);
588        } else {
589            // Insert at the specified entry position
590            let target_entry = &entries[index];
591            let insertion_index = target_entry.0.index();
592            self.0
593                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
594        }
595    }
596
597    /// Insert a value before the first entry with the specified key
598    ///
599    /// If no entry with the specified key exists, this method does nothing.
600    ///
601    /// # Example
602    ///
603    /// ```
604    /// # use systemd_unit_edit::SystemdUnit;
605    /// # use std::str::FromStr;
606    /// let input = r#"[Unit]
607    /// Description=Test Service
608    /// After=network.target
609    /// "#;
610    /// let unit = SystemdUnit::from_str(input).unwrap();
611    /// {
612    ///     let mut section = unit.get_section("Unit").unwrap();
613    ///     section.insert_before("After", "Wants", "foo.service");
614    /// }
615    ///
616    /// let section = unit.get_section("Unit").unwrap();
617    /// let entries: Vec<_> = section.entries().collect();
618    /// assert_eq!(entries[0].key(), Some("Description".to_string()));
619    /// assert_eq!(entries[1].key(), Some("Wants".to_string()));
620    /// assert_eq!(entries[2].key(), Some("After".to_string()));
621    /// ```
622    pub fn insert_before(&mut self, existing_key: &str, key: &str, value: &str) {
623        let new_entry = Entry::new(key, value);
624
625        // Find the first entry with the matching key
626        let target_entry = self
627            .entries()
628            .find(|e| e.key().as_deref() == Some(existing_key));
629
630        if let Some(entry) = target_entry {
631            let insertion_index = entry.0.index();
632            self.0
633                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
634        }
635        // If the key doesn't exist, do nothing
636    }
637
638    /// Insert a value after the first entry with the specified key
639    ///
640    /// If no entry with the specified key exists, this method does nothing.
641    ///
642    /// # Example
643    ///
644    /// ```
645    /// # use systemd_unit_edit::SystemdUnit;
646    /// # use std::str::FromStr;
647    /// let input = r#"[Unit]
648    /// Description=Test Service
649    /// After=network.target
650    /// "#;
651    /// let unit = SystemdUnit::from_str(input).unwrap();
652    /// {
653    ///     let mut section = unit.get_section("Unit").unwrap();
654    ///     section.insert_after("Description", "Wants", "foo.service");
655    /// }
656    ///
657    /// let section = unit.get_section("Unit").unwrap();
658    /// let entries: Vec<_> = section.entries().collect();
659    /// assert_eq!(entries[0].key(), Some("Description".to_string()));
660    /// assert_eq!(entries[1].key(), Some("Wants".to_string()));
661    /// assert_eq!(entries[2].key(), Some("After".to_string()));
662    /// ```
663    pub fn insert_after(&mut self, existing_key: &str, key: &str, value: &str) {
664        let new_entry = Entry::new(key, value);
665
666        // Find the first entry with the matching key
667        let target_entry = self
668            .entries()
669            .find(|e| e.key().as_deref() == Some(existing_key));
670
671        if let Some(entry) = target_entry {
672            let insertion_index = entry.0.index() + 1;
673            self.0
674                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
675        }
676        // If the key doesn't exist, do nothing
677    }
678
679    /// Set a space-separated list value for a key
680    ///
681    /// This is a convenience method for setting list-type directives
682    /// (e.g., `Wants=`, `After=`). The values will be joined with spaces.
683    ///
684    /// # Example
685    ///
686    /// ```
687    /// # use systemd_unit_edit::SystemdUnit;
688    /// # use std::str::FromStr;
689    /// # let mut unit = SystemdUnit::from_str("[Unit]\n").unwrap();
690    /// # let mut section = unit.get_section("Unit").unwrap();
691    /// section.set_list("Wants", &["foo.service", "bar.service"]);
692    /// // Results in: Wants=foo.service bar.service
693    /// ```
694    pub fn set_list(&mut self, key: &str, values: &[&str]) {
695        let value = values.join(" ");
696        self.set(key, &value);
697    }
698
699    /// Get a value parsed as a space-separated list
700    ///
701    /// This is a convenience method for getting list-type directives.
702    /// If the key doesn't exist, returns an empty vector.
703    pub fn get_list(&self, key: &str) -> Vec<String> {
704        self.entries()
705            .find(|e| e.key().as_deref() == Some(key))
706            .map(|e| e.value_as_list())
707            .unwrap_or_default()
708    }
709
710    /// Get a value parsed as a boolean
711    ///
712    /// Returns `None` if the key doesn't exist or if the value is not a valid boolean.
713    ///
714    /// # Example
715    ///
716    /// ```
717    /// # use systemd_unit_edit::SystemdUnit;
718    /// # use std::str::FromStr;
719    /// let unit = SystemdUnit::from_str("[Service]\nRemainAfterExit=yes\n").unwrap();
720    /// let section = unit.get_section("Service").unwrap();
721    /// assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
722    /// ```
723    pub fn get_bool(&self, key: &str) -> Option<bool> {
724        self.entries()
725            .find(|e| e.key().as_deref() == Some(key))
726            .and_then(|e| e.value_as_bool())
727    }
728
729    /// Set a boolean value for a key
730    ///
731    /// This is a convenience method that formats the boolean as "yes" or "no".
732    ///
733    /// # Example
734    ///
735    /// ```
736    /// # use systemd_unit_edit::SystemdUnit;
737    /// # use std::str::FromStr;
738    /// let unit = SystemdUnit::from_str("[Service]\n").unwrap();
739    /// let mut section = unit.get_section("Service").unwrap();
740    /// section.set_bool("RemainAfterExit", true);
741    /// assert_eq!(section.get("RemainAfterExit"), Some("yes".to_string()));
742    /// ```
743    pub fn set_bool(&mut self, key: &str, value: bool) {
744        self.set(key, Entry::format_bool(value));
745    }
746
747    /// Remove the first entry with the given key
748    pub fn remove(&mut self, key: &str) {
749        // Find and remove the first entry with the matching key
750        let entry_to_remove = self.0.children().find_map(|child| {
751            let entry = Entry::cast(child)?;
752            if entry.key().as_deref() == Some(key) {
753                Some(entry)
754            } else {
755                None
756            }
757        });
758
759        if let Some(entry) = entry_to_remove {
760            entry.syntax().detach();
761        }
762    }
763
764    /// Remove all entries with the given key
765    pub fn remove_all(&mut self, key: &str) {
766        // Collect all entries to remove first (can't mutate while iterating)
767        let entries_to_remove: Vec<_> = self
768            .0
769            .children()
770            .filter_map(Entry::cast)
771            .filter(|e| e.key().as_deref() == Some(key))
772            .collect();
773
774        for entry in entries_to_remove {
775            entry.syntax().detach();
776        }
777    }
778
779    /// Remove a specific value from entries with the given key
780    ///
781    /// This is useful for multi-value fields like `After=`, `Wants=`, etc.
782    /// It handles space-separated values within a single entry and removes
783    /// entire entries if they only contain the target value.
784    ///
785    /// # Example
786    ///
787    /// ```
788    /// # use systemd_unit_edit::SystemdUnit;
789    /// # use std::str::FromStr;
790    /// let input = r#"[Unit]
791    /// After=network.target syslog.target
792    /// After=remote-fs.target
793    /// "#;
794    /// let unit = SystemdUnit::from_str(input).unwrap();
795    /// {
796    ///     let mut section = unit.sections().next().unwrap();
797    ///     section.remove_value("After", "syslog.target");
798    /// }
799    ///
800    /// let section = unit.sections().next().unwrap();
801    /// let all_after = section.get_all("After");
802    /// assert_eq!(all_after.len(), 2);
803    /// assert_eq!(all_after[0], "network.target");
804    /// assert_eq!(all_after[1], "remote-fs.target");
805    /// ```
806    pub fn remove_value(&mut self, key: &str, value_to_remove: &str) {
807        // Collect all entries with the matching key
808        let entries_to_process: Vec<_> = self
809            .entries()
810            .filter(|e| e.key().as_deref() == Some(key))
811            .collect();
812
813        for entry in entries_to_process {
814            // Get the current value as a list
815            let current_list = entry.value_as_list();
816
817            // Filter out the target value
818            let new_list: Vec<_> = current_list
819                .iter()
820                .filter(|v| v.as_str() != value_to_remove)
821                .map(|s| s.as_str())
822                .collect();
823
824            if new_list.is_empty() {
825                // Remove the entire entry if no values remain
826                entry.syntax().detach();
827            } else if new_list.len() < current_list.len() {
828                // Some values were removed but some remain
829                // Create a new entry with the filtered values
830                let new_entry = Entry::new(key, &new_list.join(" "));
831
832                // Replace the old entry with the new one
833                let index = entry.0.index();
834                self.0
835                    .splice_children(index..index + 1, vec![new_entry.0.into()]);
836            }
837            // else: the value wasn't found in this entry, no change needed
838        }
839    }
840
841    /// Remove entries matching a predicate
842    ///
843    /// This provides a flexible way to remove entries based on arbitrary conditions.
844    /// The predicate receives the entry's key and value and should return `true` for
845    /// entries that should be removed.
846    ///
847    /// # Example
848    ///
849    /// ```
850    /// # use systemd_unit_edit::SystemdUnit;
851    /// # use std::str::FromStr;
852    /// let input = r#"[Unit]
853    /// After=network.target syslog.target
854    /// Wants=foo.service
855    /// After=remote-fs.target
856    /// "#;
857    /// let unit = SystemdUnit::from_str(input).unwrap();
858    /// {
859    ///     let mut section = unit.sections().next().unwrap();
860    ///     section.remove_entries_where(|key, value| {
861    ///         key == "After" && value.split_whitespace().any(|v| v == "syslog.target")
862    ///     });
863    /// }
864    ///
865    /// let section = unit.sections().next().unwrap();
866    /// let all_after = section.get_all("After");
867    /// assert_eq!(all_after.len(), 1);
868    /// assert_eq!(all_after[0], "remote-fs.target");
869    /// assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
870    /// ```
871    pub fn remove_entries_where<F>(&mut self, mut predicate: F)
872    where
873        F: FnMut(&str, &str) -> bool,
874    {
875        // Collect all entries to remove first (can't mutate while iterating)
876        let entries_to_remove: Vec<_> = self
877            .entries()
878            .filter(|entry| {
879                if let (Some(key), Some(value)) = (entry.key(), entry.value()) {
880                    predicate(&key, &value)
881                } else {
882                    false
883                }
884            })
885            .collect();
886
887        for entry in entries_to_remove {
888            entry.syntax().detach();
889        }
890    }
891
892    /// Get the raw syntax node
893    pub fn syntax(&self) -> &SyntaxNode {
894        &self.0
895    }
896}
897
898impl AstNode for Section {
899    type Language = Lang;
900
901    fn can_cast(kind: SyntaxKind) -> bool {
902        kind == SyntaxKind::SECTION
903    }
904
905    fn cast(node: SyntaxNode) -> Option<Self> {
906        if node.kind() == SyntaxKind::SECTION {
907            Some(Section(node))
908        } else {
909            None
910        }
911    }
912
913    fn syntax(&self) -> &SyntaxNode {
914        &self.0
915    }
916}
917
918/// Unescape a string by processing C-style escape sequences
919fn unescape_string(s: &str) -> String {
920    let mut result = String::new();
921    let mut chars = s.chars().peekable();
922
923    while let Some(ch) = chars.next() {
924        if ch == '\\' {
925            match chars.next() {
926                Some('n') => result.push('\n'),
927                Some('t') => result.push('\t'),
928                Some('r') => result.push('\r'),
929                Some('\\') => result.push('\\'),
930                Some('"') => result.push('"'),
931                Some('\'') => result.push('\''),
932                Some('x') => {
933                    // Hexadecimal byte: \xhh
934                    let hex: String = chars.by_ref().take(2).collect();
935                    if let Ok(byte) = u8::from_str_radix(&hex, 16) {
936                        result.push(byte as char);
937                    } else {
938                        // Invalid escape, keep as-is
939                        result.push('\\');
940                        result.push('x');
941                        result.push_str(&hex);
942                    }
943                }
944                Some('u') => {
945                    // Unicode codepoint: \unnnn
946                    let hex: String = chars.by_ref().take(4).collect();
947                    if let Ok(code) = u32::from_str_radix(&hex, 16) {
948                        if let Some(unicode_char) = char::from_u32(code) {
949                            result.push(unicode_char);
950                        } else {
951                            // Invalid codepoint, keep as-is
952                            result.push('\\');
953                            result.push('u');
954                            result.push_str(&hex);
955                        }
956                    } else {
957                        // Invalid escape, keep as-is
958                        result.push('\\');
959                        result.push('u');
960                        result.push_str(&hex);
961                    }
962                }
963                Some('U') => {
964                    // Unicode codepoint: \Unnnnnnnn
965                    let hex: String = chars.by_ref().take(8).collect();
966                    if let Ok(code) = u32::from_str_radix(&hex, 16) {
967                        if let Some(unicode_char) = char::from_u32(code) {
968                            result.push(unicode_char);
969                        } else {
970                            // Invalid codepoint, keep as-is
971                            result.push('\\');
972                            result.push('U');
973                            result.push_str(&hex);
974                        }
975                    } else {
976                        // Invalid escape, keep as-is
977                        result.push('\\');
978                        result.push('U');
979                        result.push_str(&hex);
980                    }
981                }
982                Some(c) if c.is_ascii_digit() => {
983                    // Octal byte: \nnn (up to 3 digits)
984                    let mut octal = String::from(c);
985                    for _ in 0..2 {
986                        if let Some(&next_ch) = chars.peek() {
987                            if next_ch.is_ascii_digit() && next_ch < '8' {
988                                octal.push(chars.next().unwrap());
989                            } else {
990                                break;
991                            }
992                        }
993                    }
994                    if let Ok(byte) = u8::from_str_radix(&octal, 8) {
995                        result.push(byte as char);
996                    } else {
997                        // Invalid escape, keep as-is
998                        result.push('\\');
999                        result.push_str(&octal);
1000                    }
1001                }
1002                Some(c) => {
1003                    // Unknown escape sequence, keep the backslash
1004                    result.push('\\');
1005                    result.push(c);
1006                }
1007                None => {
1008                    // Backslash at end of string
1009                    result.push('\\');
1010                }
1011            }
1012        } else {
1013            result.push(ch);
1014        }
1015    }
1016
1017    result
1018}
1019
1020/// Escape a string for use in systemd unit files
1021fn escape_string(s: &str) -> String {
1022    let mut result = String::new();
1023
1024    for ch in s.chars() {
1025        match ch {
1026            '\\' => result.push_str("\\\\"),
1027            '\n' => result.push_str("\\n"),
1028            '\t' => result.push_str("\\t"),
1029            '\r' => result.push_str("\\r"),
1030            '"' => result.push_str("\\\""),
1031            _ => result.push(ch),
1032        }
1033    }
1034
1035    result
1036}
1037
1038/// Remove quotes from a string if present
1039///
1040/// According to systemd specification, quotes (both double and single) are
1041/// removed when processing values. This function handles:
1042/// - Removing matching outer quotes
1043/// - Preserving whitespace inside quotes
1044/// - Handling escaped quotes inside quoted strings
1045fn unquote_string(s: &str) -> String {
1046    let trimmed = s.trim();
1047
1048    if trimmed.len() < 2 {
1049        return trimmed.to_string();
1050    }
1051
1052    let first = trimmed.chars().next();
1053    let last = trimmed.chars().last();
1054
1055    // Check if string is quoted with matching quotes
1056    if let (Some('"'), Some('"')) = (first, last) {
1057        // Remove outer quotes
1058        trimmed[1..trimmed.len() - 1].to_string()
1059    } else if let (Some('\''), Some('\'')) = (first, last) {
1060        // Remove outer quotes
1061        trimmed[1..trimmed.len() - 1].to_string()
1062    } else {
1063        // Not quoted, return as-is (but trimmed)
1064        trimmed.to_string()
1065    }
1066}
1067
1068/// A key-value entry in a section
1069#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1070pub struct Entry(SyntaxNode);
1071
1072impl Entry {
1073    /// Create a new entry with key=value
1074    pub fn new(key: &str, value: &str) -> Entry {
1075        use rowan::GreenNodeBuilder;
1076
1077        let mut builder = GreenNodeBuilder::new();
1078        builder.start_node(SyntaxKind::ENTRY.into());
1079        builder.token(SyntaxKind::KEY.into(), key);
1080        builder.token(SyntaxKind::EQUALS.into(), "=");
1081        builder.token(SyntaxKind::VALUE.into(), value);
1082        builder.token(SyntaxKind::NEWLINE.into(), "\n");
1083        builder.finish_node();
1084        Entry(SyntaxNode::new_root_mut(builder.finish()))
1085    }
1086
1087    /// Get the key name
1088    pub fn key(&self) -> Option<String> {
1089        let key_token = self
1090            .0
1091            .children_with_tokens()
1092            .find(|e| e.kind() == SyntaxKind::KEY)?;
1093        Some(key_token.as_token()?.text().to_string())
1094    }
1095
1096    /// Get the value (handles line continuations)
1097    pub fn value(&self) -> Option<String> {
1098        // Find all VALUE tokens after EQUALS, handling line continuations
1099        let mut found_equals = false;
1100        let mut value_parts = Vec::new();
1101
1102        for element in self.0.children_with_tokens() {
1103            match element.kind() {
1104                SyntaxKind::EQUALS => found_equals = true,
1105                SyntaxKind::VALUE if found_equals => {
1106                    value_parts.push(element.as_token()?.text().to_string());
1107                }
1108                SyntaxKind::LINE_CONTINUATION if found_equals => {
1109                    // Line continuation: backslash-newline is replaced with a space
1110                    // But don't add a space if the last value part already ends with whitespace
1111                    let should_add_space = value_parts
1112                        .last()
1113                        .map(|s| !s.ends_with(' ') && !s.ends_with('\t'))
1114                        .unwrap_or(true);
1115                    if should_add_space {
1116                        value_parts.push(" ".to_string());
1117                    }
1118                }
1119                SyntaxKind::WHITESPACE if found_equals && !value_parts.is_empty() => {
1120                    // Only include whitespace that's part of the value (after we've started collecting)
1121                    // Skip leading whitespace immediately after EQUALS
1122                    value_parts.push(element.as_token()?.text().to_string());
1123                }
1124                SyntaxKind::NEWLINE => break,
1125                _ => {}
1126            }
1127        }
1128
1129        if value_parts.is_empty() {
1130            None
1131        } else {
1132            // Join all value parts (line continuations already converted to spaces)
1133            Some(value_parts.join(""))
1134        }
1135    }
1136
1137    /// Get the raw value as it appears in the file (including line continuations)
1138    pub fn raw_value(&self) -> Option<String> {
1139        let mut found_equals = false;
1140        let mut value_parts = Vec::new();
1141
1142        for element in self.0.children_with_tokens() {
1143            match element.kind() {
1144                SyntaxKind::EQUALS => found_equals = true,
1145                SyntaxKind::VALUE if found_equals => {
1146                    value_parts.push(element.as_token()?.text().to_string());
1147                }
1148                SyntaxKind::LINE_CONTINUATION if found_equals => {
1149                    value_parts.push(element.as_token()?.text().to_string());
1150                }
1151                SyntaxKind::WHITESPACE if found_equals => {
1152                    value_parts.push(element.as_token()?.text().to_string());
1153                }
1154                SyntaxKind::NEWLINE => break,
1155                _ => {}
1156            }
1157        }
1158
1159        if value_parts.is_empty() {
1160            None
1161        } else {
1162            Some(value_parts.join(""))
1163        }
1164    }
1165
1166    /// Get the value with escape sequences processed
1167    ///
1168    /// This processes C-style escape sequences as defined in the systemd specification:
1169    /// - `\n` - newline
1170    /// - `\t` - tab
1171    /// - `\r` - carriage return
1172    /// - `\\` - backslash
1173    /// - `\"` - double quote
1174    /// - `\'` - single quote
1175    /// - `\xhh` - hexadecimal byte (2 digits)
1176    /// - `\nnn` - octal byte (3 digits)
1177    /// - `\unnnn` - Unicode codepoint (4 hex digits)
1178    /// - `\Unnnnnnnn` - Unicode codepoint (8 hex digits)
1179    pub fn unescape_value(&self) -> Option<String> {
1180        let value = self.value()?;
1181        Some(unescape_string(&value))
1182    }
1183
1184    /// Escape a string value for use in systemd unit files
1185    ///
1186    /// This escapes special characters that need escaping in systemd values:
1187    /// - backslash (`\`) becomes `\\`
1188    /// - newline (`\n`) becomes `\n`
1189    /// - tab (`\t`) becomes `\t`
1190    /// - carriage return (`\r`) becomes `\r`
1191    /// - double quote (`"`) becomes `\"`
1192    pub fn escape_value(value: &str) -> String {
1193        escape_string(value)
1194    }
1195
1196    /// Check if the value is quoted (starts and ends with matching quotes)
1197    ///
1198    /// Returns the quote character if the value is quoted, None otherwise.
1199    /// Systemd supports both double quotes (`"`) and single quotes (`'`).
1200    pub fn is_quoted(&self) -> Option<char> {
1201        let value = self.value()?;
1202        let trimmed = value.trim();
1203
1204        if trimmed.len() < 2 {
1205            return None;
1206        }
1207
1208        let first = trimmed.chars().next()?;
1209        let last = trimmed.chars().last()?;
1210
1211        if (first == '"' || first == '\'') && first == last {
1212            Some(first)
1213        } else {
1214            None
1215        }
1216    }
1217
1218    /// Get the value with quotes removed (if present)
1219    ///
1220    /// According to systemd specification, quotes are removed when processing values.
1221    /// This method returns the value with outer quotes stripped if present.
1222    pub fn unquoted_value(&self) -> Option<String> {
1223        let value = self.value()?;
1224        Some(unquote_string(&value))
1225    }
1226
1227    /// Get the value with quotes preserved as they appear in the file
1228    ///
1229    /// This is useful when you want to preserve the exact quoting style.
1230    pub fn quoted_value(&self) -> Option<String> {
1231        // This is the same as value() - just provided for clarity
1232        self.value()
1233    }
1234
1235    /// Parse the value as a space-separated list
1236    ///
1237    /// Many systemd directives use space-separated lists (e.g., `Wants=`,
1238    /// `After=`, `Before=`). This method splits the value on whitespace
1239    /// and returns a vector of strings.
1240    ///
1241    /// Empty values return an empty vector.
1242    pub fn value_as_list(&self) -> Vec<String> {
1243        let value = match self.unquoted_value() {
1244            Some(v) => v,
1245            None => return Vec::new(),
1246        };
1247
1248        value.split_whitespace().map(|s| s.to_string()).collect()
1249    }
1250
1251    /// Parse the value as a boolean
1252    ///
1253    /// According to systemd specification, boolean values accept:
1254    /// - Positive: `1`, `yes`, `true`, `on`
1255    /// - Negative: `0`, `no`, `false`, `off`
1256    ///
1257    /// Returns `None` if the value is not a valid boolean or if the entry has no value.
1258    ///
1259    /// # Example
1260    ///
1261    /// ```
1262    /// # use systemd_unit_edit::SystemdUnit;
1263    /// # use std::str::FromStr;
1264    /// let unit = SystemdUnit::from_str("[Service]\nRemainAfterExit=yes\n").unwrap();
1265    /// let section = unit.get_section("Service").unwrap();
1266    /// let entry = section.entries().next().unwrap();
1267    /// assert_eq!(entry.value_as_bool(), Some(true));
1268    /// ```
1269    pub fn value_as_bool(&self) -> Option<bool> {
1270        let value = self.unquoted_value()?;
1271        let value_lower = value.trim().to_lowercase();
1272
1273        match value_lower.as_str() {
1274            "1" | "yes" | "true" | "on" => Some(true),
1275            "0" | "no" | "false" | "off" => Some(false),
1276            _ => None,
1277        }
1278    }
1279
1280    /// Format a boolean value for use in systemd unit files
1281    ///
1282    /// This converts a boolean to the canonical systemd format:
1283    /// - `true` becomes `"yes"`
1284    /// - `false` becomes `"no"`
1285    ///
1286    /// # Example
1287    ///
1288    /// ```
1289    /// # use systemd_unit_edit::Entry;
1290    /// assert_eq!(Entry::format_bool(true), "yes");
1291    /// assert_eq!(Entry::format_bool(false), "no");
1292    /// ```
1293    pub fn format_bool(value: bool) -> &'static str {
1294        if value {
1295            "yes"
1296        } else {
1297            "no"
1298        }
1299    }
1300
1301    /// Expand systemd specifiers in the value
1302    ///
1303    /// This replaces systemd specifiers like `%i`, `%u`, `%h` with their
1304    /// values from the provided context.
1305    ///
1306    /// # Example
1307    ///
1308    /// ```
1309    /// # use systemd_unit_edit::{SystemdUnit, SpecifierContext};
1310    /// # use std::str::FromStr;
1311    /// let unit = SystemdUnit::from_str("[Service]\nWorkingDirectory=/var/lib/%i\n").unwrap();
1312    /// let section = unit.get_section("Service").unwrap();
1313    /// let entry = section.entries().next().unwrap();
1314    ///
1315    /// let mut ctx = SpecifierContext::new();
1316    /// ctx.set("i", "myinstance");
1317    ///
1318    /// assert_eq!(entry.expand_specifiers(&ctx), Some("/var/lib/myinstance".to_string()));
1319    /// ```
1320    pub fn expand_specifiers(
1321        &self,
1322        context: &crate::specifier::SpecifierContext,
1323    ) -> Option<String> {
1324        let value = self.value()?;
1325        Some(context.expand(&value))
1326    }
1327
1328    /// Set a new value for this entry, modifying it in place
1329    ///
1330    /// This replaces the entry's value while preserving its key and position
1331    /// in the section. This is useful when iterating over entries and modifying
1332    /// them selectively.
1333    ///
1334    /// # Example
1335    ///
1336    /// ```
1337    /// # use systemd_unit_edit::SystemdUnit;
1338    /// # use std::str::FromStr;
1339    /// let input = r#"[Unit]
1340    /// After=network.target syslog.target
1341    /// Wants=foo.service
1342    /// After=remote-fs.target
1343    /// "#;
1344    /// let unit = SystemdUnit::from_str(input).unwrap();
1345    /// let section = unit.get_section("Unit").unwrap();
1346    ///
1347    /// for entry in section.entries() {
1348    ///     if entry.key().as_deref() == Some("After") {
1349    ///         let values = entry.value_as_list();
1350    ///         let filtered: Vec<_> = values.iter()
1351    ///             .filter(|v| v.as_str() != "syslog.target")
1352    ///             .map(|s| s.as_str())
1353    ///             .collect();
1354    ///         entry.set_value(&filtered.join(" "));
1355    ///     }
1356    /// }
1357    ///
1358    /// let section = unit.get_section("Unit").unwrap();
1359    /// assert_eq!(section.get_all("After"), vec!["network.target", "remote-fs.target"]);
1360    /// ```
1361    pub fn set_value(&self, new_value: &str) {
1362        let key = self.key().expect("Entry should have a key");
1363        let new_entry = Entry::new(&key, new_value);
1364
1365        // Get parent and replace this entry
1366        let parent = self.0.parent().expect("Entry should have a parent");
1367        let index = self.0.index();
1368        parent.splice_children(index..index + 1, vec![new_entry.0.into()]);
1369    }
1370
1371    /// Get the raw syntax node
1372    pub fn syntax(&self) -> &SyntaxNode {
1373        &self.0
1374    }
1375}
1376
1377impl AstNode for Entry {
1378    type Language = Lang;
1379
1380    fn can_cast(kind: SyntaxKind) -> bool {
1381        kind == SyntaxKind::ENTRY
1382    }
1383
1384    fn cast(node: SyntaxNode) -> Option<Self> {
1385        if node.kind() == SyntaxKind::ENTRY {
1386            Some(Entry(node))
1387        } else {
1388            None
1389        }
1390    }
1391
1392    fn syntax(&self) -> &SyntaxNode {
1393        &self.0
1394    }
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399    use super::*;
1400
1401    #[test]
1402    fn test_parse_simple() {
1403        let input = r#"[Unit]
1404Description=Test Service
1405After=network.target
1406"#;
1407        let unit = SystemdUnit::from_str(input).unwrap();
1408        assert_eq!(unit.sections().count(), 1);
1409
1410        let section = unit.sections().next().unwrap();
1411        assert_eq!(section.name(), Some("Unit".to_string()));
1412        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1413        assert_eq!(section.get("After"), Some("network.target".to_string()));
1414    }
1415
1416    #[test]
1417    fn test_parse_with_comments() {
1418        let input = r#"# Top comment
1419[Unit]
1420# Comment before description
1421Description=Test Service
1422; Semicolon comment
1423After=network.target
1424"#;
1425        let unit = SystemdUnit::from_str(input).unwrap();
1426        assert_eq!(unit.sections().count(), 1);
1427
1428        let section = unit.sections().next().unwrap();
1429        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1430    }
1431
1432    #[test]
1433    fn test_parse_multiple_sections() {
1434        let input = r#"[Unit]
1435Description=Test Service
1436
1437[Service]
1438Type=simple
1439ExecStart=/usr/bin/test
1440
1441[Install]
1442WantedBy=multi-user.target
1443"#;
1444        let unit = SystemdUnit::from_str(input).unwrap();
1445        assert_eq!(unit.sections().count(), 3);
1446
1447        let unit_section = unit.get_section("Unit").unwrap();
1448        assert_eq!(
1449            unit_section.get("Description"),
1450            Some("Test Service".to_string())
1451        );
1452
1453        let service_section = unit.get_section("Service").unwrap();
1454        assert_eq!(service_section.get("Type"), Some("simple".to_string()));
1455        assert_eq!(
1456            service_section.get("ExecStart"),
1457            Some("/usr/bin/test".to_string())
1458        );
1459
1460        let install_section = unit.get_section("Install").unwrap();
1461        assert_eq!(
1462            install_section.get("WantedBy"),
1463            Some("multi-user.target".to_string())
1464        );
1465    }
1466
1467    #[test]
1468    fn test_parse_with_spaces() {
1469        let input = "[Unit]\nDescription = Test Service\n";
1470        let unit = SystemdUnit::from_str(input).unwrap();
1471
1472        let section = unit.sections().next().unwrap();
1473        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1474    }
1475
1476    #[test]
1477    fn test_line_continuation() {
1478        let input = "[Service]\nExecStart=/bin/echo \\\n  hello world\n";
1479        let unit = SystemdUnit::from_str(input).unwrap();
1480
1481        let section = unit.sections().next().unwrap();
1482        let entry = section.entries().next().unwrap();
1483        assert_eq!(entry.key(), Some("ExecStart".to_string()));
1484        // Line continuation: backslash is replaced with space
1485        assert_eq!(entry.value(), Some("/bin/echo   hello world".to_string()));
1486    }
1487
1488    #[test]
1489    fn test_lossless_roundtrip() {
1490        let input = r#"# Comment
1491[Unit]
1492Description=Test Service
1493After=network.target
1494
1495[Service]
1496Type=simple
1497ExecStart=/usr/bin/test
1498"#;
1499        let unit = SystemdUnit::from_str(input).unwrap();
1500        let output = unit.text();
1501        assert_eq!(input, output);
1502    }
1503
1504    #[test]
1505    fn test_set_value() {
1506        let input = r#"[Unit]
1507Description=Test Service
1508"#;
1509        let unit = SystemdUnit::from_str(input).unwrap();
1510        {
1511            let mut section = unit.sections().next().unwrap();
1512            section.set("Description", "Updated Service");
1513        }
1514
1515        let section = unit.sections().next().unwrap();
1516        assert_eq!(
1517            section.get("Description"),
1518            Some("Updated Service".to_string())
1519        );
1520    }
1521
1522    #[test]
1523    fn test_add_new_entry() {
1524        let input = r#"[Unit]
1525Description=Test Service
1526"#;
1527        let unit = SystemdUnit::from_str(input).unwrap();
1528        {
1529            let mut section = unit.sections().next().unwrap();
1530            section.set("After", "network.target");
1531        }
1532
1533        let section = unit.sections().next().unwrap();
1534        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1535        assert_eq!(section.get("After"), Some("network.target".to_string()));
1536    }
1537
1538    #[test]
1539    fn test_multiple_values_same_key() {
1540        let input = r#"[Unit]
1541Wants=foo.service
1542Wants=bar.service
1543"#;
1544        let unit = SystemdUnit::from_str(input).unwrap();
1545        let section = unit.sections().next().unwrap();
1546
1547        // get() returns the first value
1548        assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
1549
1550        // get_all() returns all values
1551        let all_wants = section.get_all("Wants");
1552        assert_eq!(all_wants.len(), 2);
1553        assert_eq!(all_wants[0], "foo.service");
1554        assert_eq!(all_wants[1], "bar.service");
1555    }
1556
1557    #[test]
1558    fn test_add_multiple_entries() {
1559        let input = r#"[Unit]
1560Description=Test Service
1561"#;
1562        let unit = SystemdUnit::from_str(input).unwrap();
1563        {
1564            let mut section = unit.sections().next().unwrap();
1565            section.add("Wants", "foo.service");
1566            section.add("Wants", "bar.service");
1567        }
1568
1569        let section = unit.sections().next().unwrap();
1570        let all_wants = section.get_all("Wants");
1571        assert_eq!(all_wants.len(), 2);
1572        assert_eq!(all_wants[0], "foo.service");
1573        assert_eq!(all_wants[1], "bar.service");
1574    }
1575
1576    #[test]
1577    fn test_remove_entry() {
1578        let input = r#"[Unit]
1579Description=Test Service
1580After=network.target
1581"#;
1582        let unit = SystemdUnit::from_str(input).unwrap();
1583        {
1584            let mut section = unit.sections().next().unwrap();
1585            section.remove("After");
1586        }
1587
1588        let section = unit.sections().next().unwrap();
1589        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1590        assert_eq!(section.get("After"), None);
1591    }
1592
1593    #[test]
1594    fn test_remove_all_entries() {
1595        let input = r#"[Unit]
1596Wants=foo.service
1597Wants=bar.service
1598Description=Test
1599"#;
1600        let unit = SystemdUnit::from_str(input).unwrap();
1601        {
1602            let mut section = unit.sections().next().unwrap();
1603            section.remove_all("Wants");
1604        }
1605
1606        let section = unit.sections().next().unwrap();
1607        assert_eq!(section.get_all("Wants").len(), 0);
1608        assert_eq!(section.get("Description"), Some("Test".to_string()));
1609    }
1610
1611    #[test]
1612    fn test_unescape_basic() {
1613        let input = r#"[Unit]
1614Description=Test\nService
1615"#;
1616        let unit = SystemdUnit::from_str(input).unwrap();
1617        let section = unit.sections().next().unwrap();
1618        let entry = section.entries().next().unwrap();
1619
1620        assert_eq!(entry.value(), Some("Test\\nService".to_string()));
1621        assert_eq!(entry.unescape_value(), Some("Test\nService".to_string()));
1622    }
1623
1624    #[test]
1625    fn test_unescape_all_escapes() {
1626        let input = r#"[Unit]
1627Value=\n\t\r\\\"\'\x41\101\u0041\U00000041
1628"#;
1629        let unit = SystemdUnit::from_str(input).unwrap();
1630        let section = unit.sections().next().unwrap();
1631        let entry = section.entries().next().unwrap();
1632
1633        let unescaped = entry.unescape_value().unwrap();
1634        // \n = newline, \t = tab, \r = carriage return, \\ = backslash
1635        // \" = quote, \' = single quote
1636        // \x41 = 'A', \101 = 'A', \u0041 = 'A', \U00000041 = 'A'
1637        assert_eq!(unescaped, "\n\t\r\\\"'AAAA");
1638    }
1639
1640    #[test]
1641    fn test_unescape_unicode() {
1642        let input = r#"[Unit]
1643Value=Hello\u0020World\U0001F44D
1644"#;
1645        let unit = SystemdUnit::from_str(input).unwrap();
1646        let section = unit.sections().next().unwrap();
1647        let entry = section.entries().next().unwrap();
1648
1649        let unescaped = entry.unescape_value().unwrap();
1650        // \u0020 = space, \U0001F44D = 👍
1651        assert_eq!(unescaped, "Hello World👍");
1652    }
1653
1654    #[test]
1655    fn test_escape_value() {
1656        let text = "Hello\nWorld\t\"Test\"\\Path";
1657        let escaped = Entry::escape_value(text);
1658        assert_eq!(escaped, "Hello\\nWorld\\t\\\"Test\\\"\\\\Path");
1659    }
1660
1661    #[test]
1662    fn test_escape_unescape_roundtrip() {
1663        let original = "Test\nwith\ttabs\rand\"quotes\"\\backslash";
1664        let escaped = Entry::escape_value(original);
1665        let unescaped = unescape_string(&escaped);
1666        assert_eq!(original, unescaped);
1667    }
1668
1669    #[test]
1670    fn test_unescape_invalid_sequences() {
1671        // Invalid escape sequences should be kept as-is or handled gracefully
1672        let input = r#"[Unit]
1673Value=\z\xFF\u12\U1234
1674"#;
1675        let unit = SystemdUnit::from_str(input).unwrap();
1676        let section = unit.sections().next().unwrap();
1677        let entry = section.entries().next().unwrap();
1678
1679        let unescaped = entry.unescape_value().unwrap();
1680        // \z is unknown, \xFF has only 2 chars but needs hex, \u12 and \U1234 are incomplete
1681        assert!(unescaped.contains("\\z"));
1682    }
1683
1684    #[test]
1685    fn test_quoted_double_quotes() {
1686        let input = r#"[Unit]
1687Description="Test Service"
1688"#;
1689        let unit = SystemdUnit::from_str(input).unwrap();
1690        let section = unit.sections().next().unwrap();
1691        let entry = section.entries().next().unwrap();
1692
1693        assert_eq!(entry.value(), Some("\"Test Service\"".to_string()));
1694        assert_eq!(entry.quoted_value(), Some("\"Test Service\"".to_string()));
1695        assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1696        assert_eq!(entry.is_quoted(), Some('"'));
1697    }
1698
1699    #[test]
1700    fn test_quoted_single_quotes() {
1701        let input = r#"[Unit]
1702Description='Test Service'
1703"#;
1704        let unit = SystemdUnit::from_str(input).unwrap();
1705        let section = unit.sections().next().unwrap();
1706        let entry = section.entries().next().unwrap();
1707
1708        assert_eq!(entry.value(), Some("'Test Service'".to_string()));
1709        assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1710        assert_eq!(entry.is_quoted(), Some('\''));
1711    }
1712
1713    #[test]
1714    fn test_quoted_with_whitespace() {
1715        let input = r#"[Unit]
1716Description="  Test Service  "
1717"#;
1718        let unit = SystemdUnit::from_str(input).unwrap();
1719        let section = unit.sections().next().unwrap();
1720        let entry = section.entries().next().unwrap();
1721
1722        // Quotes preserve internal whitespace
1723        assert_eq!(entry.unquoted_value(), Some("  Test Service  ".to_string()));
1724    }
1725
1726    #[test]
1727    fn test_unquoted_value() {
1728        let input = r#"[Unit]
1729Description=Test Service
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        assert_eq!(entry.value(), Some("Test Service".to_string()));
1736        assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1737        assert_eq!(entry.is_quoted(), None);
1738    }
1739
1740    #[test]
1741    fn test_mismatched_quotes() {
1742        let input = r#"[Unit]
1743Description="Test Service'
1744"#;
1745        let unit = SystemdUnit::from_str(input).unwrap();
1746        let section = unit.sections().next().unwrap();
1747        let entry = section.entries().next().unwrap();
1748
1749        // Mismatched quotes should not be considered quoted
1750        assert_eq!(entry.is_quoted(), None);
1751        assert_eq!(entry.unquoted_value(), Some("\"Test Service'".to_string()));
1752    }
1753
1754    #[test]
1755    fn test_empty_quotes() {
1756        let input = r#"[Unit]
1757Description=""
1758"#;
1759        let unit = SystemdUnit::from_str(input).unwrap();
1760        let section = unit.sections().next().unwrap();
1761        let entry = section.entries().next().unwrap();
1762
1763        assert_eq!(entry.is_quoted(), Some('"'));
1764        assert_eq!(entry.unquoted_value(), Some("".to_string()));
1765    }
1766
1767    #[test]
1768    fn test_value_as_list() {
1769        let input = r#"[Unit]
1770After=network.target remote-fs.target
1771"#;
1772        let unit = SystemdUnit::from_str(input).unwrap();
1773        let section = unit.sections().next().unwrap();
1774        let entry = section.entries().next().unwrap();
1775
1776        let list = entry.value_as_list();
1777        assert_eq!(list.len(), 2);
1778        assert_eq!(list[0], "network.target");
1779        assert_eq!(list[1], "remote-fs.target");
1780    }
1781
1782    #[test]
1783    fn test_value_as_list_single() {
1784        let input = r#"[Unit]
1785After=network.target
1786"#;
1787        let unit = SystemdUnit::from_str(input).unwrap();
1788        let section = unit.sections().next().unwrap();
1789        let entry = section.entries().next().unwrap();
1790
1791        let list = entry.value_as_list();
1792        assert_eq!(list.len(), 1);
1793        assert_eq!(list[0], "network.target");
1794    }
1795
1796    #[test]
1797    fn test_value_as_list_empty() {
1798        let input = r#"[Unit]
1799After=
1800"#;
1801        let unit = SystemdUnit::from_str(input).unwrap();
1802        let section = unit.sections().next().unwrap();
1803        let entry = section.entries().next().unwrap();
1804
1805        let list = entry.value_as_list();
1806        assert_eq!(list.len(), 0);
1807    }
1808
1809    #[test]
1810    fn test_value_as_list_with_extra_whitespace() {
1811        let input = r#"[Unit]
1812After=  network.target   remote-fs.target
1813"#;
1814        let unit = SystemdUnit::from_str(input).unwrap();
1815        let section = unit.sections().next().unwrap();
1816        let entry = section.entries().next().unwrap();
1817
1818        let list = entry.value_as_list();
1819        assert_eq!(list.len(), 2);
1820        assert_eq!(list[0], "network.target");
1821        assert_eq!(list[1], "remote-fs.target");
1822    }
1823
1824    #[test]
1825    fn test_section_get_list() {
1826        let input = r#"[Unit]
1827After=network.target remote-fs.target
1828"#;
1829        let unit = SystemdUnit::from_str(input).unwrap();
1830        let section = unit.sections().next().unwrap();
1831
1832        let list = section.get_list("After");
1833        assert_eq!(list.len(), 2);
1834        assert_eq!(list[0], "network.target");
1835        assert_eq!(list[1], "remote-fs.target");
1836    }
1837
1838    #[test]
1839    fn test_section_get_list_missing() {
1840        let input = r#"[Unit]
1841Description=Test
1842"#;
1843        let unit = SystemdUnit::from_str(input).unwrap();
1844        let section = unit.sections().next().unwrap();
1845
1846        let list = section.get_list("After");
1847        assert_eq!(list.len(), 0);
1848    }
1849
1850    #[test]
1851    fn test_section_set_list() {
1852        let input = r#"[Unit]
1853Description=Test
1854"#;
1855        let unit = SystemdUnit::from_str(input).unwrap();
1856        {
1857            let mut section = unit.sections().next().unwrap();
1858            section.set_list("After", &["network.target", "remote-fs.target"]);
1859        }
1860
1861        let section = unit.sections().next().unwrap();
1862        let list = section.get_list("After");
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_section_set_list_replaces() {
1870        let input = r#"[Unit]
1871After=foo.target
1872"#;
1873        let unit = SystemdUnit::from_str(input).unwrap();
1874        {
1875            let mut section = unit.sections().next().unwrap();
1876            section.set_list("After", &["network.target", "remote-fs.target"]);
1877        }
1878
1879        let section = unit.sections().next().unwrap();
1880        let list = section.get_list("After");
1881        assert_eq!(list.len(), 2);
1882        assert_eq!(list[0], "network.target");
1883        assert_eq!(list[1], "remote-fs.target");
1884    }
1885
1886    #[test]
1887    fn test_value_as_bool_positive() {
1888        let inputs = vec!["yes", "true", "1", "on", "YES", "True", "ON"];
1889
1890        for input_val in inputs {
1891            let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1892            let unit = SystemdUnit::from_str(&input).unwrap();
1893            let section = unit.sections().next().unwrap();
1894            let entry = section.entries().next().unwrap();
1895            assert_eq!(
1896                entry.value_as_bool(),
1897                Some(true),
1898                "Failed for input: {}",
1899                input_val
1900            );
1901        }
1902    }
1903
1904    #[test]
1905    fn test_value_as_bool_negative() {
1906        let inputs = vec!["no", "false", "0", "off", "NO", "False", "OFF"];
1907
1908        for input_val in inputs {
1909            let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1910            let unit = SystemdUnit::from_str(&input).unwrap();
1911            let section = unit.sections().next().unwrap();
1912            let entry = section.entries().next().unwrap();
1913            assert_eq!(
1914                entry.value_as_bool(),
1915                Some(false),
1916                "Failed for input: {}",
1917                input_val
1918            );
1919        }
1920    }
1921
1922    #[test]
1923    fn test_value_as_bool_invalid() {
1924        let input = r#"[Service]
1925RemainAfterExit=maybe
1926"#;
1927        let unit = SystemdUnit::from_str(input).unwrap();
1928        let section = unit.sections().next().unwrap();
1929        let entry = section.entries().next().unwrap();
1930        assert_eq!(entry.value_as_bool(), None);
1931    }
1932
1933    #[test]
1934    fn test_value_as_bool_with_whitespace() {
1935        let input = r#"[Service]
1936RemainAfterExit=  yes
1937"#;
1938        let unit = SystemdUnit::from_str(input).unwrap();
1939        let section = unit.sections().next().unwrap();
1940        let entry = section.entries().next().unwrap();
1941        assert_eq!(entry.value_as_bool(), Some(true));
1942    }
1943
1944    #[test]
1945    fn test_format_bool() {
1946        assert_eq!(Entry::format_bool(true), "yes");
1947        assert_eq!(Entry::format_bool(false), "no");
1948    }
1949
1950    #[test]
1951    fn test_section_get_bool() {
1952        let input = r#"[Service]
1953RemainAfterExit=yes
1954Type=simple
1955"#;
1956        let unit = SystemdUnit::from_str(input).unwrap();
1957        let section = unit.sections().next().unwrap();
1958
1959        assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1960        assert_eq!(section.get_bool("Type"), None); // Not a boolean
1961        assert_eq!(section.get_bool("Missing"), None); // Doesn't exist
1962    }
1963
1964    #[test]
1965    fn test_section_set_bool() {
1966        let input = r#"[Service]
1967Type=simple
1968"#;
1969        let unit = SystemdUnit::from_str(input).unwrap();
1970        {
1971            let mut section = unit.sections().next().unwrap();
1972            section.set_bool("RemainAfterExit", true);
1973            section.set_bool("PrivateTmp", false);
1974        }
1975
1976        let section = unit.sections().next().unwrap();
1977        assert_eq!(section.get("RemainAfterExit"), Some("yes".to_string()));
1978        assert_eq!(section.get("PrivateTmp"), Some("no".to_string()));
1979        assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1980        assert_eq!(section.get_bool("PrivateTmp"), Some(false));
1981    }
1982
1983    #[test]
1984    fn test_add_entry_with_trailing_whitespace() {
1985        // Section with trailing blank lines
1986        let input = r#"[Unit]
1987Description=Test Service
1988
1989"#;
1990        let unit = SystemdUnit::from_str(input).unwrap();
1991        {
1992            let mut section = unit.sections().next().unwrap();
1993            section.add("After", "network.target");
1994        }
1995
1996        let output = unit.text();
1997        // New entry should be added immediately after the last entry, not after whitespace
1998        let expected = r#"[Unit]
1999Description=Test Service
2000After=network.target
2001
2002"#;
2003        assert_eq!(output, expected);
2004    }
2005
2006    #[test]
2007    fn test_set_new_entry_with_trailing_whitespace() {
2008        // Section with trailing blank lines
2009        let input = r#"[Unit]
2010Description=Test Service
2011
2012"#;
2013        let unit = SystemdUnit::from_str(input).unwrap();
2014        {
2015            let mut section = unit.sections().next().unwrap();
2016            section.set("After", "network.target");
2017        }
2018
2019        let output = unit.text();
2020        // New entry should be added immediately after the last entry, not after whitespace
2021        let expected = r#"[Unit]
2022Description=Test Service
2023After=network.target
2024
2025"#;
2026        assert_eq!(output, expected);
2027    }
2028
2029    #[test]
2030    fn test_remove_value_from_space_separated_list() {
2031        let input = r#"[Unit]
2032After=network.target syslog.target remote-fs.target
2033"#;
2034        let unit = SystemdUnit::from_str(input).unwrap();
2035        {
2036            let mut section = unit.sections().next().unwrap();
2037            section.remove_value("After", "syslog.target");
2038        }
2039
2040        let section = unit.sections().next().unwrap();
2041        assert_eq!(
2042            section.get("After"),
2043            Some("network.target remote-fs.target".to_string())
2044        );
2045    }
2046
2047    #[test]
2048    fn test_remove_value_removes_entire_entry() {
2049        let input = r#"[Unit]
2050After=syslog.target
2051Description=Test
2052"#;
2053        let unit = SystemdUnit::from_str(input).unwrap();
2054        {
2055            let mut section = unit.sections().next().unwrap();
2056            section.remove_value("After", "syslog.target");
2057        }
2058
2059        let section = unit.sections().next().unwrap();
2060        assert_eq!(section.get("After"), None);
2061        assert_eq!(section.get("Description"), Some("Test".to_string()));
2062    }
2063
2064    #[test]
2065    fn test_remove_value_from_multiple_entries() {
2066        let input = r#"[Unit]
2067After=network.target syslog.target
2068After=remote-fs.target
2069After=syslog.target multi-user.target
2070"#;
2071        let unit = SystemdUnit::from_str(input).unwrap();
2072        {
2073            let mut section = unit.sections().next().unwrap();
2074            section.remove_value("After", "syslog.target");
2075        }
2076
2077        let section = unit.sections().next().unwrap();
2078        let all_after = section.get_all("After");
2079        assert_eq!(all_after.len(), 3);
2080        assert_eq!(all_after[0], "network.target");
2081        assert_eq!(all_after[1], "remote-fs.target");
2082        assert_eq!(all_after[2], "multi-user.target");
2083    }
2084
2085    #[test]
2086    fn test_remove_value_not_found() {
2087        let input = r#"[Unit]
2088After=network.target remote-fs.target
2089"#;
2090        let unit = SystemdUnit::from_str(input).unwrap();
2091        {
2092            let mut section = unit.sections().next().unwrap();
2093            section.remove_value("After", "nonexistent.target");
2094        }
2095
2096        let section = unit.sections().next().unwrap();
2097        // Should remain unchanged
2098        assert_eq!(
2099            section.get("After"),
2100            Some("network.target remote-fs.target".to_string())
2101        );
2102    }
2103
2104    #[test]
2105    fn test_remove_value_preserves_order() {
2106        let input = r#"[Unit]
2107Description=Test Service
2108After=network.target syslog.target
2109Wants=foo.service
2110After=remote-fs.target
2111Requires=bar.service
2112"#;
2113        let unit = SystemdUnit::from_str(input).unwrap();
2114        {
2115            let mut section = unit.sections().next().unwrap();
2116            section.remove_value("After", "syslog.target");
2117        }
2118
2119        let section = unit.sections().next().unwrap();
2120        let entries: Vec<_> = section.entries().collect();
2121
2122        // Verify order is preserved
2123        assert_eq!(entries[0].key(), Some("Description".to_string()));
2124        assert_eq!(entries[1].key(), Some("After".to_string()));
2125        assert_eq!(entries[1].value(), Some("network.target".to_string()));
2126        assert_eq!(entries[2].key(), Some("Wants".to_string()));
2127        assert_eq!(entries[3].key(), Some("After".to_string()));
2128        assert_eq!(entries[3].value(), Some("remote-fs.target".to_string()));
2129        assert_eq!(entries[4].key(), Some("Requires".to_string()));
2130    }
2131
2132    #[test]
2133    fn test_remove_value_key_not_found() {
2134        let input = r#"[Unit]
2135Description=Test Service
2136"#;
2137        let unit = SystemdUnit::from_str(input).unwrap();
2138        {
2139            let mut section = unit.sections().next().unwrap();
2140            section.remove_value("After", "network.target");
2141        }
2142
2143        // Should not panic or error, just no-op
2144        let section = unit.sections().next().unwrap();
2145        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
2146        assert_eq!(section.get("After"), None);
2147    }
2148
2149    #[test]
2150    fn test_entry_set_value_basic() {
2151        let input = r#"[Unit]
2152After=network.target
2153Description=Test
2154"#;
2155        let unit = SystemdUnit::from_str(input).unwrap();
2156        let section = unit.get_section("Unit").unwrap();
2157
2158        for entry in section.entries() {
2159            if entry.key().as_deref() == Some("After") {
2160                entry.set_value("remote-fs.target");
2161            }
2162        }
2163
2164        let section = unit.get_section("Unit").unwrap();
2165        assert_eq!(section.get("After"), Some("remote-fs.target".to_string()));
2166        assert_eq!(section.get("Description"), Some("Test".to_string()));
2167    }
2168
2169    #[test]
2170    fn test_entry_set_value_preserves_order() {
2171        let input = r#"[Unit]
2172Description=Test Service
2173After=network.target syslog.target
2174Wants=foo.service
2175After=remote-fs.target
2176Requires=bar.service
2177"#;
2178        let unit = SystemdUnit::from_str(input).unwrap();
2179        let section = unit.get_section("Unit").unwrap();
2180
2181        for entry in section.entries() {
2182            if entry.key().as_deref() == Some("After") {
2183                let values = entry.value_as_list();
2184                let filtered: Vec<_> = values
2185                    .iter()
2186                    .filter(|v| v.as_str() != "syslog.target")
2187                    .map(|s| s.as_str())
2188                    .collect();
2189                if !filtered.is_empty() {
2190                    entry.set_value(&filtered.join(" "));
2191                }
2192            }
2193        }
2194
2195        let section = unit.get_section("Unit").unwrap();
2196        let entries: Vec<_> = section.entries().collect();
2197
2198        // Verify order is preserved
2199        assert_eq!(entries[0].key(), Some("Description".to_string()));
2200        assert_eq!(entries[1].key(), Some("After".to_string()));
2201        assert_eq!(entries[1].value(), Some("network.target".to_string()));
2202        assert_eq!(entries[2].key(), Some("Wants".to_string()));
2203        assert_eq!(entries[3].key(), Some("After".to_string()));
2204        assert_eq!(entries[3].value(), Some("remote-fs.target".to_string()));
2205        assert_eq!(entries[4].key(), Some("Requires".to_string()));
2206    }
2207
2208    #[test]
2209    fn test_entry_set_value_multiple_entries() {
2210        let input = r#"[Unit]
2211After=network.target
2212After=syslog.target
2213After=remote-fs.target
2214"#;
2215        let unit = SystemdUnit::from_str(input).unwrap();
2216        let section = unit.get_section("Unit").unwrap();
2217
2218        // Collect entries first to avoid iterator invalidation issues
2219        let entries_to_modify: Vec<_> = section
2220            .entries()
2221            .filter(|e| e.key().as_deref() == Some("After"))
2222            .collect();
2223
2224        // Modify all After entries
2225        for entry in entries_to_modify {
2226            let old_value = entry.value().unwrap();
2227            entry.set_value(&format!("{} multi-user.target", old_value));
2228        }
2229
2230        let section = unit.get_section("Unit").unwrap();
2231        let all_after = section.get_all("After");
2232        assert_eq!(all_after.len(), 3);
2233        assert_eq!(all_after[0], "network.target multi-user.target");
2234        assert_eq!(all_after[1], "syslog.target multi-user.target");
2235        assert_eq!(all_after[2], "remote-fs.target multi-user.target");
2236    }
2237
2238    #[test]
2239    fn test_entry_set_value_with_empty_string() {
2240        let input = r#"[Unit]
2241After=network.target
2242"#;
2243        let unit = SystemdUnit::from_str(input).unwrap();
2244        let section = unit.get_section("Unit").unwrap();
2245
2246        for entry in section.entries() {
2247            if entry.key().as_deref() == Some("After") {
2248                entry.set_value("");
2249            }
2250        }
2251
2252        let section = unit.get_section("Unit").unwrap();
2253        assert_eq!(section.get("After"), Some("".to_string()));
2254    }
2255
2256    #[test]
2257    fn test_remove_entries_where_basic() {
2258        let input = r#"[Unit]
2259After=network.target
2260Wants=foo.service
2261After=syslog.target
2262"#;
2263        let unit = SystemdUnit::from_str(input).unwrap();
2264        {
2265            let mut section = unit.sections().next().unwrap();
2266            section.remove_entries_where(|key, _value| key == "After");
2267        }
2268
2269        let section = unit.sections().next().unwrap();
2270        assert_eq!(section.get_all("After").len(), 0);
2271        assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
2272    }
2273
2274    #[test]
2275    fn test_remove_entries_where_with_value_check() {
2276        let input = r#"[Unit]
2277After=network.target syslog.target
2278Wants=foo.service
2279After=remote-fs.target
2280"#;
2281        let unit = SystemdUnit::from_str(input).unwrap();
2282        {
2283            let mut section = unit.sections().next().unwrap();
2284            section.remove_entries_where(|key, value| {
2285                key == "After" && value.split_whitespace().any(|v| v == "syslog.target")
2286            });
2287        }
2288
2289        let section = unit.sections().next().unwrap();
2290        let all_after = section.get_all("After");
2291        assert_eq!(all_after.len(), 1);
2292        assert_eq!(all_after[0], "remote-fs.target");
2293        assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
2294    }
2295
2296    #[test]
2297    fn test_remove_entries_where_preserves_order() {
2298        let input = r#"[Unit]
2299Description=Test Service
2300After=network.target
2301Wants=foo.service
2302After=syslog.target
2303Requires=bar.service
2304After=remote-fs.target
2305"#;
2306        let unit = SystemdUnit::from_str(input).unwrap();
2307        {
2308            let mut section = unit.sections().next().unwrap();
2309            section.remove_entries_where(|key, value| key == "After" && value.contains("syslog"));
2310        }
2311
2312        let section = unit.sections().next().unwrap();
2313        let entries: Vec<_> = section.entries().collect();
2314
2315        assert_eq!(entries.len(), 5);
2316        assert_eq!(entries[0].key(), Some("Description".to_string()));
2317        assert_eq!(entries[1].key(), Some("After".to_string()));
2318        assert_eq!(entries[1].value(), Some("network.target".to_string()));
2319        assert_eq!(entries[2].key(), Some("Wants".to_string()));
2320        assert_eq!(entries[3].key(), Some("Requires".to_string()));
2321        assert_eq!(entries[4].key(), Some("After".to_string()));
2322        assert_eq!(entries[4].value(), Some("remote-fs.target".to_string()));
2323    }
2324
2325    #[test]
2326    fn test_remove_entries_where_no_matches() {
2327        let input = r#"[Unit]
2328After=network.target
2329Wants=foo.service
2330"#;
2331        let unit = SystemdUnit::from_str(input).unwrap();
2332        {
2333            let mut section = unit.sections().next().unwrap();
2334            section.remove_entries_where(|key, _value| key == "Requires");
2335        }
2336
2337        let section = unit.sections().next().unwrap();
2338        assert_eq!(section.get("After"), Some("network.target".to_string()));
2339        assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
2340    }
2341
2342    #[test]
2343    fn test_remove_entries_where_all_entries() {
2344        let input = r#"[Unit]
2345After=network.target
2346Wants=foo.service
2347Requires=bar.service
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| true);
2353        }
2354
2355        let section = unit.sections().next().unwrap();
2356        assert_eq!(section.entries().count(), 0);
2357    }
2358
2359    #[test]
2360    fn test_remove_entries_where_complex_predicate() {
2361        let input = r#"[Unit]
2362After=network.target
2363After=syslog.target remote-fs.target
2364Wants=foo.service
2365After=multi-user.target
2366Requires=bar.service
2367"#;
2368        let unit = SystemdUnit::from_str(input).unwrap();
2369        {
2370            let mut section = unit.sections().next().unwrap();
2371            // Remove After entries with multiple space-separated values
2372            section.remove_entries_where(|key, value| {
2373                key == "After" && value.split_whitespace().count() > 1
2374            });
2375        }
2376
2377        let section = unit.sections().next().unwrap();
2378        let all_after = section.get_all("After");
2379        assert_eq!(all_after.len(), 2);
2380        assert_eq!(all_after[0], "network.target");
2381        assert_eq!(all_after[1], "multi-user.target");
2382    }
2383
2384    #[test]
2385    fn test_insert_at_beginning() {
2386        let input = r#"[Unit]
2387Description=Test Service
2388After=network.target
2389"#;
2390        let unit = SystemdUnit::from_str(input).unwrap();
2391        {
2392            let mut section = unit.sections().next().unwrap();
2393            section.insert_at(0, "Wants", "foo.service");
2394        }
2395
2396        let section = unit.sections().next().unwrap();
2397        let entries: Vec<_> = section.entries().collect();
2398        assert_eq!(entries.len(), 3);
2399        assert_eq!(entries[0].key(), Some("Wants".to_string()));
2400        assert_eq!(entries[0].value(), Some("foo.service".to_string()));
2401        assert_eq!(entries[1].key(), Some("Description".to_string()));
2402        assert_eq!(entries[2].key(), Some("After".to_string()));
2403    }
2404
2405    #[test]
2406    fn test_insert_at_middle() {
2407        let input = r#"[Unit]
2408Description=Test Service
2409After=network.target
2410"#;
2411        let unit = SystemdUnit::from_str(input).unwrap();
2412        {
2413            let mut section = unit.sections().next().unwrap();
2414            section.insert_at(1, "Wants", "foo.service");
2415        }
2416
2417        let section = unit.sections().next().unwrap();
2418        let entries: Vec<_> = section.entries().collect();
2419        assert_eq!(entries.len(), 3);
2420        assert_eq!(entries[0].key(), Some("Description".to_string()));
2421        assert_eq!(entries[1].key(), Some("Wants".to_string()));
2422        assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2423        assert_eq!(entries[2].key(), Some("After".to_string()));
2424    }
2425
2426    #[test]
2427    fn test_insert_at_end() {
2428        let input = r#"[Unit]
2429Description=Test Service
2430After=network.target
2431"#;
2432        let unit = SystemdUnit::from_str(input).unwrap();
2433        {
2434            let mut section = unit.sections().next().unwrap();
2435            section.insert_at(2, "Wants", "foo.service");
2436        }
2437
2438        let section = unit.sections().next().unwrap();
2439        let entries: Vec<_> = section.entries().collect();
2440        assert_eq!(entries.len(), 3);
2441        assert_eq!(entries[0].key(), Some("Description".to_string()));
2442        assert_eq!(entries[1].key(), Some("After".to_string()));
2443        assert_eq!(entries[2].key(), Some("Wants".to_string()));
2444        assert_eq!(entries[2].value(), Some("foo.service".to_string()));
2445    }
2446
2447    #[test]
2448    fn test_insert_at_beyond_end() {
2449        let input = r#"[Unit]
2450Description=Test Service
2451"#;
2452        let unit = SystemdUnit::from_str(input).unwrap();
2453        {
2454            let mut section = unit.sections().next().unwrap();
2455            section.insert_at(100, "Wants", "foo.service");
2456        }
2457
2458        let section = unit.sections().next().unwrap();
2459        let entries: Vec<_> = section.entries().collect();
2460        assert_eq!(entries.len(), 2);
2461        assert_eq!(entries[0].key(), Some("Description".to_string()));
2462        assert_eq!(entries[1].key(), Some("Wants".to_string()));
2463        assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2464    }
2465
2466    #[test]
2467    fn test_insert_at_empty_section() {
2468        let input = r#"[Unit]
2469"#;
2470        let unit = SystemdUnit::from_str(input).unwrap();
2471        {
2472            let mut section = unit.sections().next().unwrap();
2473            section.insert_at(0, "Description", "Test Service");
2474        }
2475
2476        let section = unit.sections().next().unwrap();
2477        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
2478    }
2479
2480    #[test]
2481    fn test_insert_before_basic() {
2482        let input = r#"[Unit]
2483Description=Test Service
2484After=network.target
2485"#;
2486        let unit = SystemdUnit::from_str(input).unwrap();
2487        {
2488            let mut section = unit.sections().next().unwrap();
2489            section.insert_before("After", "Wants", "foo.service");
2490        }
2491
2492        let section = unit.sections().next().unwrap();
2493        let entries: Vec<_> = section.entries().collect();
2494        assert_eq!(entries.len(), 3);
2495        assert_eq!(entries[0].key(), Some("Description".to_string()));
2496        assert_eq!(entries[1].key(), Some("Wants".to_string()));
2497        assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2498        assert_eq!(entries[2].key(), Some("After".to_string()));
2499    }
2500
2501    #[test]
2502    fn test_insert_before_first_entry() {
2503        let input = r#"[Unit]
2504Description=Test Service
2505After=network.target
2506"#;
2507        let unit = SystemdUnit::from_str(input).unwrap();
2508        {
2509            let mut section = unit.sections().next().unwrap();
2510            section.insert_before("Description", "Wants", "foo.service");
2511        }
2512
2513        let section = unit.sections().next().unwrap();
2514        let entries: Vec<_> = section.entries().collect();
2515        assert_eq!(entries.len(), 3);
2516        assert_eq!(entries[0].key(), Some("Wants".to_string()));
2517        assert_eq!(entries[0].value(), Some("foo.service".to_string()));
2518        assert_eq!(entries[1].key(), Some("Description".to_string()));
2519        assert_eq!(entries[2].key(), Some("After".to_string()));
2520    }
2521
2522    #[test]
2523    fn test_insert_before_nonexistent_key() {
2524        let input = r#"[Unit]
2525Description=Test Service
2526"#;
2527        let unit = SystemdUnit::from_str(input).unwrap();
2528        {
2529            let mut section = unit.sections().next().unwrap();
2530            section.insert_before("After", "Wants", "foo.service");
2531        }
2532
2533        let section = unit.sections().next().unwrap();
2534        let entries: Vec<_> = section.entries().collect();
2535        assert_eq!(entries.len(), 1);
2536        assert_eq!(entries[0].key(), Some("Description".to_string()));
2537    }
2538
2539    #[test]
2540    fn test_insert_before_multiple_occurrences() {
2541        let input = r#"[Unit]
2542After=network.target
2543After=syslog.target
2544"#;
2545        let unit = SystemdUnit::from_str(input).unwrap();
2546        {
2547            let mut section = unit.sections().next().unwrap();
2548            section.insert_before("After", "Wants", "foo.service");
2549        }
2550
2551        let section = unit.sections().next().unwrap();
2552        let entries: Vec<_> = section.entries().collect();
2553        assert_eq!(entries.len(), 3);
2554        assert_eq!(entries[0].key(), Some("Wants".to_string()));
2555        assert_eq!(entries[1].key(), Some("After".to_string()));
2556        assert_eq!(entries[1].value(), Some("network.target".to_string()));
2557        assert_eq!(entries[2].key(), Some("After".to_string()));
2558        assert_eq!(entries[2].value(), Some("syslog.target".to_string()));
2559    }
2560
2561    #[test]
2562    fn test_insert_after_basic() {
2563        let input = r#"[Unit]
2564Description=Test Service
2565After=network.target
2566"#;
2567        let unit = SystemdUnit::from_str(input).unwrap();
2568        {
2569            let mut section = unit.sections().next().unwrap();
2570            section.insert_after("Description", "Wants", "foo.service");
2571        }
2572
2573        let section = unit.sections().next().unwrap();
2574        let entries: Vec<_> = section.entries().collect();
2575        assert_eq!(entries.len(), 3);
2576        assert_eq!(entries[0].key(), Some("Description".to_string()));
2577        assert_eq!(entries[1].key(), Some("Wants".to_string()));
2578        assert_eq!(entries[1].value(), Some("foo.service".to_string()));
2579        assert_eq!(entries[2].key(), Some("After".to_string()));
2580    }
2581
2582    #[test]
2583    fn test_insert_after_last_entry() {
2584        let input = r#"[Unit]
2585Description=Test Service
2586After=network.target
2587"#;
2588        let unit = SystemdUnit::from_str(input).unwrap();
2589        {
2590            let mut section = unit.sections().next().unwrap();
2591            section.insert_after("After", "Wants", "foo.service");
2592        }
2593
2594        let section = unit.sections().next().unwrap();
2595        let entries: Vec<_> = section.entries().collect();
2596        assert_eq!(entries.len(), 3);
2597        assert_eq!(entries[0].key(), Some("Description".to_string()));
2598        assert_eq!(entries[1].key(), Some("After".to_string()));
2599        assert_eq!(entries[2].key(), Some("Wants".to_string()));
2600        assert_eq!(entries[2].value(), Some("foo.service".to_string()));
2601    }
2602
2603    #[test]
2604    fn test_insert_after_nonexistent_key() {
2605        let input = r#"[Unit]
2606Description=Test Service
2607"#;
2608        let unit = SystemdUnit::from_str(input).unwrap();
2609        {
2610            let mut section = unit.sections().next().unwrap();
2611            section.insert_after("After", "Wants", "foo.service");
2612        }
2613
2614        let section = unit.sections().next().unwrap();
2615        let entries: Vec<_> = section.entries().collect();
2616        assert_eq!(entries.len(), 1);
2617        assert_eq!(entries[0].key(), Some("Description".to_string()));
2618    }
2619
2620    #[test]
2621    fn test_insert_after_multiple_occurrences() {
2622        let input = r#"[Unit]
2623After=network.target
2624After=syslog.target
2625"#;
2626        let unit = SystemdUnit::from_str(input).unwrap();
2627        {
2628            let mut section = unit.sections().next().unwrap();
2629            section.insert_after("After", "Wants", "foo.service");
2630        }
2631
2632        let section = unit.sections().next().unwrap();
2633        let entries: Vec<_> = section.entries().collect();
2634        assert_eq!(entries.len(), 3);
2635        assert_eq!(entries[0].key(), Some("After".to_string()));
2636        assert_eq!(entries[0].value(), Some("network.target".to_string()));
2637        assert_eq!(entries[1].key(), Some("Wants".to_string()));
2638        assert_eq!(entries[2].key(), Some("After".to_string()));
2639        assert_eq!(entries[2].value(), Some("syslog.target".to_string()));
2640    }
2641
2642    #[test]
2643    fn test_insert_preserves_whitespace() {
2644        let input = r#"[Unit]
2645Description=Test Service
2646
2647After=network.target
2648"#;
2649        let unit = SystemdUnit::from_str(input).unwrap();
2650        {
2651            let mut section = unit.sections().next().unwrap();
2652            section.insert_at(1, "Wants", "foo.service");
2653        }
2654
2655        let section = unit.sections().next().unwrap();
2656        let entries: Vec<_> = section.entries().collect();
2657        assert_eq!(entries.len(), 3);
2658        assert_eq!(entries[0].key(), Some("Description".to_string()));
2659        assert_eq!(entries[1].key(), Some("Wants".to_string()));
2660        assert_eq!(entries[2].key(), Some("After".to_string()));
2661
2662        let expected = r#"[Unit]
2663Description=Test Service
2664
2665Wants=foo.service
2666After=network.target
2667"#;
2668        assert_eq!(unit.text(), expected);
2669    }
2670}