Skip to main content

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