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