systemd_unit_edit/
unit.rs

1//! Parser for systemd unit files.
2//!
3//! This parser can be used to parse systemd unit files (as specified
4//! by the [systemd.syntax(7)](https://www.freedesktop.org/software/systemd/man/latest/systemd.syntax.html)),
5//! while preserving all whitespace and comments. It is based on
6//! the [rowan] library, which is a lossless parser library for Rust.
7//!
8//! Once parsed, the file can be traversed or modified, and then written back to a file.
9//!
10//! # Example
11//!
12//! ```
13//! use systemd_unit_edit::SystemdUnit;
14//! use std::str::FromStr;
15//!
16//! # let input = r#"[Unit]
17//! # Description=Test Service
18//! # After=network.target
19//! #
20//! # [Service]
21//! # Type=simple
22//! # ExecStart=/usr/bin/test
23//! # "#;
24//! # let unit = SystemdUnit::from_str(input).unwrap();
25//! # assert_eq!(unit.sections().count(), 2);
26//! # let section = unit.sections().next().unwrap();
27//! # assert_eq!(section.name(), Some("Unit".to_string()));
28//! ```
29
30use crate::lex::{lex, SyntaxKind};
31use rowan::ast::AstNode;
32use rowan::{GreenNode, GreenNodeBuilder};
33use std::path::Path;
34use std::str::FromStr;
35
36/// A positioned parse error containing location information.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct PositionedParseError {
39    /// The error message
40    pub message: String,
41    /// The text range where the error occurred
42    pub range: rowan::TextRange,
43    /// Optional error code for categorization
44    pub code: Option<String>,
45}
46
47impl std::fmt::Display for PositionedParseError {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{}", self.message)
50    }
51}
52
53impl std::error::Error for PositionedParseError {}
54
55/// List of encountered syntax errors.
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct ParseError(pub Vec<String>);
58
59impl std::fmt::Display for ParseError {
60    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
61        for err in &self.0 {
62            writeln!(f, "{}", err)?;
63        }
64        Ok(())
65    }
66}
67
68impl std::error::Error for ParseError {}
69
70/// Error parsing systemd unit files
71#[derive(Debug)]
72pub enum Error {
73    /// A syntax error was encountered while parsing the file.
74    ParseError(ParseError),
75
76    /// An I/O error was encountered while reading the file.
77    IoError(std::io::Error),
78}
79
80impl std::fmt::Display for Error {
81    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
82        match &self {
83            Error::ParseError(err) => write!(f, "{}", err),
84            Error::IoError(err) => write!(f, "{}", err),
85        }
86    }
87}
88
89impl From<ParseError> for Error {
90    fn from(err: ParseError) -> Self {
91        Self::ParseError(err)
92    }
93}
94
95impl From<std::io::Error> for Error {
96    fn from(err: std::io::Error) -> Self {
97        Self::IoError(err)
98    }
99}
100
101impl std::error::Error for Error {}
102
103/// Language definition for rowan
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
105pub enum Lang {}
106
107impl rowan::Language for Lang {
108    type Kind = SyntaxKind;
109
110    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
111        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
112    }
113
114    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
115        kind.into()
116    }
117}
118
119/// Internal parse result
120pub(crate) struct ParseResult {
121    pub(crate) green_node: GreenNode,
122    pub(crate) errors: Vec<String>,
123    pub(crate) positioned_errors: Vec<PositionedParseError>,
124}
125
126/// Parse a systemd unit file
127pub(crate) fn parse(text: &str) -> ParseResult {
128    struct Parser<'a> {
129        tokens: Vec<(SyntaxKind, &'a str)>,
130        builder: GreenNodeBuilder<'static>,
131        errors: Vec<String>,
132        positioned_errors: Vec<PositionedParseError>,
133        pos: usize,
134    }
135
136    impl<'a> Parser<'a> {
137        fn current(&self) -> Option<SyntaxKind> {
138            if self.pos < self.tokens.len() {
139                Some(self.tokens[self.tokens.len() - 1 - self.pos].0)
140            } else {
141                None
142            }
143        }
144
145        fn bump(&mut self) {
146            if self.pos < self.tokens.len() {
147                let (kind, text) = self.tokens[self.tokens.len() - 1 - self.pos];
148                self.builder.token(kind.into(), text);
149                self.pos += 1;
150            }
151        }
152
153        fn skip_ws(&mut self) {
154            while self.current() == Some(SyntaxKind::WHITESPACE) {
155                self.bump();
156            }
157        }
158
159        fn skip_blank_lines(&mut self) {
160            while let Some(kind) = self.current() {
161                match kind {
162                    SyntaxKind::NEWLINE => {
163                        self.builder.start_node(SyntaxKind::BLANK_LINE.into());
164                        self.bump();
165                        self.builder.finish_node();
166                    }
167                    SyntaxKind::WHITESPACE => {
168                        // Check if followed by newline
169                        if self.pos + 1 < self.tokens.len()
170                            && self.tokens[self.tokens.len() - 2 - self.pos].0
171                                == SyntaxKind::NEWLINE
172                        {
173                            self.builder.start_node(SyntaxKind::BLANK_LINE.into());
174                            self.bump(); // whitespace
175                            self.bump(); // newline
176                            self.builder.finish_node();
177                        } else {
178                            break;
179                        }
180                    }
181                    _ => break,
182                }
183            }
184        }
185
186        fn parse_section_header(&mut self) {
187            self.builder.start_node(SyntaxKind::SECTION_HEADER.into());
188
189            // Consume '['
190            if self.current() == Some(SyntaxKind::LEFT_BRACKET) {
191                self.bump();
192            } else {
193                self.errors
194                    .push("expected '[' at start of section header".to_string());
195            }
196
197            // Consume section name
198            if self.current() == Some(SyntaxKind::SECTION_NAME) {
199                self.bump();
200            } else {
201                self.errors
202                    .push("expected section name in section header".to_string());
203            }
204
205            // Consume ']'
206            if self.current() == Some(SyntaxKind::RIGHT_BRACKET) {
207                self.bump();
208            } else {
209                self.errors
210                    .push("expected ']' at end of section header".to_string());
211            }
212
213            // Consume newline if present
214            if self.current() == Some(SyntaxKind::NEWLINE) {
215                self.bump();
216            }
217
218            self.builder.finish_node();
219        }
220
221        fn parse_entry(&mut self) {
222            self.builder.start_node(SyntaxKind::ENTRY.into());
223
224            // Handle comment before entry
225            if self.current() == Some(SyntaxKind::COMMENT) {
226                self.bump();
227                if self.current() == Some(SyntaxKind::NEWLINE) {
228                    self.bump();
229                }
230                self.builder.finish_node();
231                return;
232            }
233
234            // Parse key
235            if self.current() == Some(SyntaxKind::KEY) {
236                self.bump();
237            } else {
238                self.errors
239                    .push(format!("expected key, got {:?}", self.current()));
240            }
241
242            self.skip_ws();
243
244            // Parse '='
245            if self.current() == Some(SyntaxKind::EQUALS) {
246                self.bump();
247            } else {
248                self.errors.push("expected '=' after key".to_string());
249            }
250
251            self.skip_ws();
252
253            // Parse value (may include line continuations)
254            while let Some(kind) = self.current() {
255                match kind {
256                    SyntaxKind::VALUE => self.bump(),
257                    SyntaxKind::LINE_CONTINUATION => {
258                        self.bump();
259                        // After line continuation, skip leading whitespace
260                        self.skip_ws();
261                    }
262                    SyntaxKind::NEWLINE => {
263                        self.bump();
264                        break;
265                    }
266                    _ => break,
267                }
268            }
269
270            self.builder.finish_node();
271        }
272
273        fn parse_section(&mut self) {
274            self.builder.start_node(SyntaxKind::SECTION.into());
275
276            // Parse section header
277            self.parse_section_header();
278
279            // Parse entries until we hit another section header or EOF
280            while let Some(kind) = self.current() {
281                match kind {
282                    SyntaxKind::LEFT_BRACKET => break, // Start of next section
283                    SyntaxKind::KEY | SyntaxKind::COMMENT => self.parse_entry(),
284                    SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE => {
285                        self.skip_blank_lines();
286                    }
287                    _ => {
288                        self.errors
289                            .push(format!("unexpected token in section: {:?}", kind));
290                        self.bump();
291                    }
292                }
293            }
294
295            self.builder.finish_node();
296        }
297
298        fn parse_file(&mut self) {
299            self.builder.start_node(SyntaxKind::ROOT.into());
300
301            // Skip leading blank lines and comments
302            while let Some(kind) = self.current() {
303                match kind {
304                    SyntaxKind::COMMENT => {
305                        self.builder.start_node(SyntaxKind::ENTRY.into());
306                        self.bump();
307                        if self.current() == Some(SyntaxKind::NEWLINE) {
308                            self.bump();
309                        }
310                        self.builder.finish_node();
311                    }
312                    SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE => {
313                        self.skip_blank_lines();
314                    }
315                    _ => break,
316                }
317            }
318
319            // Parse sections
320            while self.current().is_some() {
321                if self.current() == Some(SyntaxKind::LEFT_BRACKET) {
322                    self.parse_section();
323                } else {
324                    self.errors
325                        .push(format!("expected section header, got {:?}", self.current()));
326                    self.bump();
327                }
328            }
329
330            self.builder.finish_node();
331        }
332    }
333
334    let mut tokens: Vec<_> = lex(text).collect();
335    tokens.reverse();
336
337    let mut parser = Parser {
338        tokens,
339        builder: GreenNodeBuilder::new(),
340        errors: Vec::new(),
341        positioned_errors: Vec::new(),
342        pos: 0,
343    };
344
345    parser.parse_file();
346
347    ParseResult {
348        green_node: parser.builder.finish(),
349        errors: parser.errors,
350        positioned_errors: parser.positioned_errors,
351    }
352}
353
354// Type aliases for convenience
355type SyntaxNode = rowan::SyntaxNode<Lang>;
356
357/// The root of a systemd unit file
358#[derive(Debug, Clone, PartialEq, Eq, Hash)]
359pub struct SystemdUnit(SyntaxNode);
360
361impl SystemdUnit {
362    /// Get all sections in the file
363    pub fn sections(&self) -> impl Iterator<Item = Section> {
364        self.0.children().filter_map(Section::cast)
365    }
366
367    /// Get a specific section by name
368    pub fn get_section(&self, name: &str) -> Option<Section> {
369        self.sections().find(|s| s.name().as_deref() == Some(name))
370    }
371
372    /// Add a new section to the unit file
373    pub fn add_section(&mut self, name: &str) {
374        let new_section = Section::new(name);
375        let insertion_index = self.0.children_with_tokens().count();
376        self.0
377            .splice_children(insertion_index..insertion_index, vec![new_section.0.into()]);
378    }
379
380    /// Get the raw syntax node
381    pub fn syntax(&self) -> &SyntaxNode {
382        &self.0
383    }
384
385    /// Convert to a string (same as Display::fmt)
386    pub fn text(&self) -> String {
387        self.0.text().to_string()
388    }
389
390    /// Load from a file
391    pub fn from_file(path: &Path) -> Result<Self, Error> {
392        let text = std::fs::read_to_string(path)?;
393        Self::from_str(&text)
394    }
395
396    /// Write to a file
397    pub fn write_to_file(&self, path: &Path) -> Result<(), Error> {
398        std::fs::write(path, self.text())?;
399        Ok(())
400    }
401}
402
403impl AstNode for SystemdUnit {
404    type Language = Lang;
405
406    fn can_cast(kind: SyntaxKind) -> bool {
407        kind == SyntaxKind::ROOT
408    }
409
410    fn cast(node: SyntaxNode) -> Option<Self> {
411        if node.kind() == SyntaxKind::ROOT {
412            Some(SystemdUnit(node))
413        } else {
414            None
415        }
416    }
417
418    fn syntax(&self) -> &SyntaxNode {
419        &self.0
420    }
421}
422
423impl FromStr for SystemdUnit {
424    type Err = Error;
425
426    fn from_str(s: &str) -> Result<Self, Self::Err> {
427        let parsed = parse(s);
428        if !parsed.errors.is_empty() {
429            return Err(Error::ParseError(ParseError(parsed.errors)));
430        }
431        let node = SyntaxNode::new_root_mut(parsed.green_node);
432        Ok(SystemdUnit::cast(node).expect("root node should be SystemdUnit"))
433    }
434}
435
436impl std::fmt::Display for SystemdUnit {
437    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438        write!(f, "{}", self.0.text())
439    }
440}
441
442/// A section in a systemd unit file (e.g., [Unit], [Service])
443#[derive(Debug, Clone, PartialEq, Eq, Hash)]
444pub struct Section(SyntaxNode);
445
446impl Section {
447    /// Create a new section with the given name
448    pub fn new(name: &str) -> Section {
449        use rowan::GreenNodeBuilder;
450
451        let mut builder = GreenNodeBuilder::new();
452        builder.start_node(SyntaxKind::SECTION.into());
453
454        // Build section header
455        builder.start_node(SyntaxKind::SECTION_HEADER.into());
456        builder.token(SyntaxKind::LEFT_BRACKET.into(), "[");
457        builder.token(SyntaxKind::SECTION_NAME.into(), name);
458        builder.token(SyntaxKind::RIGHT_BRACKET.into(), "]");
459        builder.token(SyntaxKind::NEWLINE.into(), "\n");
460        builder.finish_node();
461
462        builder.finish_node();
463        Section(SyntaxNode::new_root_mut(builder.finish()))
464    }
465
466    /// Get the name of the section
467    pub fn name(&self) -> Option<String> {
468        let header = self
469            .0
470            .children()
471            .find(|n| n.kind() == SyntaxKind::SECTION_HEADER)?;
472        let value = header
473            .children_with_tokens()
474            .find(|e| e.kind() == SyntaxKind::SECTION_NAME)?;
475        Some(value.as_token()?.text().to_string())
476    }
477
478    /// Get all entries in the section
479    pub fn entries(&self) -> impl Iterator<Item = Entry> {
480        self.0.children().filter_map(Entry::cast)
481    }
482
483    /// Get a specific entry by key
484    pub fn get(&self, key: &str) -> Option<String> {
485        self.entries()
486            .find(|e| e.key().as_deref() == Some(key))
487            .and_then(|e| e.value())
488    }
489
490    /// Get all values for a key (systemd allows multiple entries with the same key)
491    pub fn get_all(&self, key: &str) -> Vec<String> {
492        self.entries()
493            .filter(|e| e.key().as_deref() == Some(key))
494            .filter_map(|e| e.value())
495            .collect()
496    }
497
498    /// Set a value for a key (replaces the first occurrence or adds if it doesn't exist)
499    pub fn set(&mut self, key: &str, value: &str) {
500        let new_entry = Entry::new(key, value);
501
502        // Check if the field already exists and replace the first occurrence
503        for entry in self.entries() {
504            if entry.key().as_deref() == Some(key) {
505                self.0.splice_children(
506                    entry.0.index()..entry.0.index() + 1,
507                    vec![new_entry.0.into()],
508                );
509                return;
510            }
511        }
512
513        // Field doesn't exist, append at the end (before the closing of the section)
514        let insertion_index = self.0.children_with_tokens().count();
515        self.0
516            .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
517    }
518
519    /// Add a value for a key (appends even if the key already exists)
520    pub fn add(&mut self, key: &str, value: &str) {
521        let new_entry = Entry::new(key, value);
522        let insertion_index = self.0.children_with_tokens().count();
523        self.0
524            .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
525    }
526
527    /// Set a space-separated list value for a key
528    ///
529    /// This is a convenience method for setting list-type directives
530    /// (e.g., `Wants=`, `After=`). The values will be joined with spaces.
531    ///
532    /// # Example
533    ///
534    /// ```
535    /// # use systemd_unit_edit::SystemdUnit;
536    /// # use std::str::FromStr;
537    /// # let mut unit = SystemdUnit::from_str("[Unit]\n").unwrap();
538    /// # let mut section = unit.get_section("Unit").unwrap();
539    /// section.set_list("Wants", &["foo.service", "bar.service"]);
540    /// // Results in: Wants=foo.service bar.service
541    /// ```
542    pub fn set_list(&mut self, key: &str, values: &[&str]) {
543        let value = values.join(" ");
544        self.set(key, &value);
545    }
546
547    /// Get a value parsed as a space-separated list
548    ///
549    /// This is a convenience method for getting list-type directives.
550    /// If the key doesn't exist, returns an empty vector.
551    pub fn get_list(&self, key: &str) -> Vec<String> {
552        self.entries()
553            .find(|e| e.key().as_deref() == Some(key))
554            .map(|e| e.value_as_list())
555            .unwrap_or_default()
556    }
557
558    /// Get a value parsed as a boolean
559    ///
560    /// Returns `None` if the key doesn't exist or if the value is not a valid boolean.
561    ///
562    /// # Example
563    ///
564    /// ```
565    /// # use systemd_unit_edit::SystemdUnit;
566    /// # use std::str::FromStr;
567    /// let unit = SystemdUnit::from_str("[Service]\nRemainAfterExit=yes\n").unwrap();
568    /// let section = unit.get_section("Service").unwrap();
569    /// assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
570    /// ```
571    pub fn get_bool(&self, key: &str) -> Option<bool> {
572        self.entries()
573            .find(|e| e.key().as_deref() == Some(key))
574            .and_then(|e| e.value_as_bool())
575    }
576
577    /// Set a boolean value for a key
578    ///
579    /// This is a convenience method that formats the boolean as "yes" or "no".
580    ///
581    /// # Example
582    ///
583    /// ```
584    /// # use systemd_unit_edit::SystemdUnit;
585    /// # use std::str::FromStr;
586    /// let unit = SystemdUnit::from_str("[Service]\n").unwrap();
587    /// let mut section = unit.get_section("Service").unwrap();
588    /// section.set_bool("RemainAfterExit", true);
589    /// assert_eq!(section.get("RemainAfterExit"), Some("yes".to_string()));
590    /// ```
591    pub fn set_bool(&mut self, key: &str, value: bool) {
592        self.set(key, Entry::format_bool(value));
593    }
594
595    /// Remove the first entry with the given key
596    pub fn remove(&mut self, key: &str) {
597        // Find and remove the first entry with the matching key
598        let entry_to_remove = self.0.children().find_map(|child| {
599            let entry = Entry::cast(child)?;
600            if entry.key().as_deref() == Some(key) {
601                Some(entry)
602            } else {
603                None
604            }
605        });
606
607        if let Some(entry) = entry_to_remove {
608            entry.syntax().detach();
609        }
610    }
611
612    /// Remove all entries with the given key
613    pub fn remove_all(&mut self, key: &str) {
614        // Collect all entries to remove first (can't mutate while iterating)
615        let entries_to_remove: Vec<_> = self
616            .0
617            .children()
618            .filter_map(Entry::cast)
619            .filter(|e| e.key().as_deref() == Some(key))
620            .collect();
621
622        for entry in entries_to_remove {
623            entry.syntax().detach();
624        }
625    }
626
627    /// Get the raw syntax node
628    pub fn syntax(&self) -> &SyntaxNode {
629        &self.0
630    }
631}
632
633impl AstNode for Section {
634    type Language = Lang;
635
636    fn can_cast(kind: SyntaxKind) -> bool {
637        kind == SyntaxKind::SECTION
638    }
639
640    fn cast(node: SyntaxNode) -> Option<Self> {
641        if node.kind() == SyntaxKind::SECTION {
642            Some(Section(node))
643        } else {
644            None
645        }
646    }
647
648    fn syntax(&self) -> &SyntaxNode {
649        &self.0
650    }
651}
652
653/// Unescape a string by processing C-style escape sequences
654fn unescape_string(s: &str) -> String {
655    let mut result = String::new();
656    let mut chars = s.chars().peekable();
657
658    while let Some(ch) = chars.next() {
659        if ch == '\\' {
660            match chars.next() {
661                Some('n') => result.push('\n'),
662                Some('t') => result.push('\t'),
663                Some('r') => result.push('\r'),
664                Some('\\') => result.push('\\'),
665                Some('"') => result.push('"'),
666                Some('\'') => result.push('\''),
667                Some('x') => {
668                    // Hexadecimal byte: \xhh
669                    let hex: String = chars.by_ref().take(2).collect();
670                    if let Ok(byte) = u8::from_str_radix(&hex, 16) {
671                        result.push(byte as char);
672                    } else {
673                        // Invalid escape, keep as-is
674                        result.push('\\');
675                        result.push('x');
676                        result.push_str(&hex);
677                    }
678                }
679                Some('u') => {
680                    // Unicode codepoint: \unnnn
681                    let hex: String = chars.by_ref().take(4).collect();
682                    if let Ok(code) = u32::from_str_radix(&hex, 16) {
683                        if let Some(unicode_char) = char::from_u32(code) {
684                            result.push(unicode_char);
685                        } else {
686                            // Invalid codepoint, keep as-is
687                            result.push('\\');
688                            result.push('u');
689                            result.push_str(&hex);
690                        }
691                    } else {
692                        // Invalid escape, keep as-is
693                        result.push('\\');
694                        result.push('u');
695                        result.push_str(&hex);
696                    }
697                }
698                Some('U') => {
699                    // Unicode codepoint: \Unnnnnnnn
700                    let hex: String = chars.by_ref().take(8).collect();
701                    if let Ok(code) = u32::from_str_radix(&hex, 16) {
702                        if let Some(unicode_char) = char::from_u32(code) {
703                            result.push(unicode_char);
704                        } else {
705                            // Invalid codepoint, keep as-is
706                            result.push('\\');
707                            result.push('U');
708                            result.push_str(&hex);
709                        }
710                    } else {
711                        // Invalid escape, keep as-is
712                        result.push('\\');
713                        result.push('U');
714                        result.push_str(&hex);
715                    }
716                }
717                Some(c) if c.is_ascii_digit() => {
718                    // Octal byte: \nnn (up to 3 digits)
719                    let mut octal = String::from(c);
720                    for _ in 0..2 {
721                        if let Some(&next_ch) = chars.peek() {
722                            if next_ch.is_ascii_digit() && next_ch < '8' {
723                                octal.push(chars.next().unwrap());
724                            } else {
725                                break;
726                            }
727                        }
728                    }
729                    if let Ok(byte) = u8::from_str_radix(&octal, 8) {
730                        result.push(byte as char);
731                    } else {
732                        // Invalid escape, keep as-is
733                        result.push('\\');
734                        result.push_str(&octal);
735                    }
736                }
737                Some(c) => {
738                    // Unknown escape sequence, keep the backslash
739                    result.push('\\');
740                    result.push(c);
741                }
742                None => {
743                    // Backslash at end of string
744                    result.push('\\');
745                }
746            }
747        } else {
748            result.push(ch);
749        }
750    }
751
752    result
753}
754
755/// Escape a string for use in systemd unit files
756fn escape_string(s: &str) -> String {
757    let mut result = String::new();
758
759    for ch in s.chars() {
760        match ch {
761            '\\' => result.push_str("\\\\"),
762            '\n' => result.push_str("\\n"),
763            '\t' => result.push_str("\\t"),
764            '\r' => result.push_str("\\r"),
765            '"' => result.push_str("\\\""),
766            _ => result.push(ch),
767        }
768    }
769
770    result
771}
772
773/// Remove quotes from a string if present
774///
775/// According to systemd specification, quotes (both double and single) are
776/// removed when processing values. This function handles:
777/// - Removing matching outer quotes
778/// - Preserving whitespace inside quotes
779/// - Handling escaped quotes inside quoted strings
780fn unquote_string(s: &str) -> String {
781    let trimmed = s.trim();
782
783    if trimmed.len() < 2 {
784        return trimmed.to_string();
785    }
786
787    let first = trimmed.chars().next();
788    let last = trimmed.chars().last();
789
790    // Check if string is quoted with matching quotes
791    if let (Some('"'), Some('"')) = (first, last) {
792        // Remove outer quotes
793        trimmed[1..trimmed.len() - 1].to_string()
794    } else if let (Some('\''), Some('\'')) = (first, last) {
795        // Remove outer quotes
796        trimmed[1..trimmed.len() - 1].to_string()
797    } else {
798        // Not quoted, return as-is (but trimmed)
799        trimmed.to_string()
800    }
801}
802
803/// A key-value entry in a section
804#[derive(Debug, Clone, PartialEq, Eq, Hash)]
805pub struct Entry(SyntaxNode);
806
807impl Entry {
808    /// Create a new entry with key=value
809    pub fn new(key: &str, value: &str) -> Entry {
810        use rowan::GreenNodeBuilder;
811
812        let mut builder = GreenNodeBuilder::new();
813        builder.start_node(SyntaxKind::ENTRY.into());
814        builder.token(SyntaxKind::KEY.into(), key);
815        builder.token(SyntaxKind::EQUALS.into(), "=");
816        builder.token(SyntaxKind::VALUE.into(), value);
817        builder.token(SyntaxKind::NEWLINE.into(), "\n");
818        builder.finish_node();
819        Entry(SyntaxNode::new_root_mut(builder.finish()))
820    }
821
822    /// Get the key name
823    pub fn key(&self) -> Option<String> {
824        let key_token = self
825            .0
826            .children_with_tokens()
827            .find(|e| e.kind() == SyntaxKind::KEY)?;
828        Some(key_token.as_token()?.text().to_string())
829    }
830
831    /// Get the value (handles line continuations)
832    pub fn value(&self) -> Option<String> {
833        // Find all VALUE tokens after EQUALS, handling line continuations
834        let mut found_equals = false;
835        let mut value_parts = Vec::new();
836
837        for element in self.0.children_with_tokens() {
838            match element.kind() {
839                SyntaxKind::EQUALS => found_equals = true,
840                SyntaxKind::VALUE if found_equals => {
841                    value_parts.push(element.as_token()?.text().to_string());
842                }
843                SyntaxKind::LINE_CONTINUATION if found_equals => {
844                    // Line continuation: backslash-newline is replaced with a space
845                    // But don't add a space if the last value part already ends with whitespace
846                    let should_add_space = value_parts
847                        .last()
848                        .map(|s| !s.ends_with(' ') && !s.ends_with('\t'))
849                        .unwrap_or(true);
850                    if should_add_space {
851                        value_parts.push(" ".to_string());
852                    }
853                }
854                SyntaxKind::WHITESPACE if found_equals && !value_parts.is_empty() => {
855                    // Only include whitespace that's part of the value (after we've started collecting)
856                    // Skip leading whitespace immediately after EQUALS
857                    value_parts.push(element.as_token()?.text().to_string());
858                }
859                SyntaxKind::NEWLINE => break,
860                _ => {}
861            }
862        }
863
864        if value_parts.is_empty() {
865            None
866        } else {
867            // Join all value parts (line continuations already converted to spaces)
868            Some(value_parts.join(""))
869        }
870    }
871
872    /// Get the raw value as it appears in the file (including line continuations)
873    pub fn raw_value(&self) -> Option<String> {
874        let mut found_equals = false;
875        let mut value_parts = Vec::new();
876
877        for element in self.0.children_with_tokens() {
878            match element.kind() {
879                SyntaxKind::EQUALS => found_equals = true,
880                SyntaxKind::VALUE if found_equals => {
881                    value_parts.push(element.as_token()?.text().to_string());
882                }
883                SyntaxKind::LINE_CONTINUATION if found_equals => {
884                    value_parts.push(element.as_token()?.text().to_string());
885                }
886                SyntaxKind::WHITESPACE if found_equals => {
887                    value_parts.push(element.as_token()?.text().to_string());
888                }
889                SyntaxKind::NEWLINE => break,
890                _ => {}
891            }
892        }
893
894        if value_parts.is_empty() {
895            None
896        } else {
897            Some(value_parts.join(""))
898        }
899    }
900
901    /// Get the value with escape sequences processed
902    ///
903    /// This processes C-style escape sequences as defined in the systemd specification:
904    /// - `\n` - newline
905    /// - `\t` - tab
906    /// - `\r` - carriage return
907    /// - `\\` - backslash
908    /// - `\"` - double quote
909    /// - `\'` - single quote
910    /// - `\xhh` - hexadecimal byte (2 digits)
911    /// - `\nnn` - octal byte (3 digits)
912    /// - `\unnnn` - Unicode codepoint (4 hex digits)
913    /// - `\Unnnnnnnn` - Unicode codepoint (8 hex digits)
914    pub fn unescape_value(&self) -> Option<String> {
915        let value = self.value()?;
916        Some(unescape_string(&value))
917    }
918
919    /// Escape a string value for use in systemd unit files
920    ///
921    /// This escapes special characters that need escaping in systemd values:
922    /// - backslash (`\`) becomes `\\`
923    /// - newline (`\n`) becomes `\n`
924    /// - tab (`\t`) becomes `\t`
925    /// - carriage return (`\r`) becomes `\r`
926    /// - double quote (`"`) becomes `\"`
927    pub fn escape_value(value: &str) -> String {
928        escape_string(value)
929    }
930
931    /// Check if the value is quoted (starts and ends with matching quotes)
932    ///
933    /// Returns the quote character if the value is quoted, None otherwise.
934    /// Systemd supports both double quotes (`"`) and single quotes (`'`).
935    pub fn is_quoted(&self) -> Option<char> {
936        let value = self.value()?;
937        let trimmed = value.trim();
938
939        if trimmed.len() < 2 {
940            return None;
941        }
942
943        let first = trimmed.chars().next()?;
944        let last = trimmed.chars().last()?;
945
946        if (first == '"' || first == '\'') && first == last {
947            Some(first)
948        } else {
949            None
950        }
951    }
952
953    /// Get the value with quotes removed (if present)
954    ///
955    /// According to systemd specification, quotes are removed when processing values.
956    /// This method returns the value with outer quotes stripped if present.
957    pub fn unquoted_value(&self) -> Option<String> {
958        let value = self.value()?;
959        Some(unquote_string(&value))
960    }
961
962    /// Get the value with quotes preserved as they appear in the file
963    ///
964    /// This is useful when you want to preserve the exact quoting style.
965    pub fn quoted_value(&self) -> Option<String> {
966        // This is the same as value() - just provided for clarity
967        self.value()
968    }
969
970    /// Parse the value as a space-separated list
971    ///
972    /// Many systemd directives use space-separated lists (e.g., `Wants=`,
973    /// `After=`, `Before=`). This method splits the value on whitespace
974    /// and returns a vector of strings.
975    ///
976    /// Empty values return an empty vector.
977    pub fn value_as_list(&self) -> Vec<String> {
978        let value = match self.unquoted_value() {
979            Some(v) => v,
980            None => return Vec::new(),
981        };
982
983        value.split_whitespace().map(|s| s.to_string()).collect()
984    }
985
986    /// Parse the value as a boolean
987    ///
988    /// According to systemd specification, boolean values accept:
989    /// - Positive: `1`, `yes`, `true`, `on`
990    /// - Negative: `0`, `no`, `false`, `off`
991    ///
992    /// Returns `None` if the value is not a valid boolean or if the entry has no value.
993    ///
994    /// # Example
995    ///
996    /// ```
997    /// # use systemd_unit_edit::SystemdUnit;
998    /// # use std::str::FromStr;
999    /// let unit = SystemdUnit::from_str("[Service]\nRemainAfterExit=yes\n").unwrap();
1000    /// let section = unit.get_section("Service").unwrap();
1001    /// let entry = section.entries().next().unwrap();
1002    /// assert_eq!(entry.value_as_bool(), Some(true));
1003    /// ```
1004    pub fn value_as_bool(&self) -> Option<bool> {
1005        let value = self.unquoted_value()?;
1006        let value_lower = value.trim().to_lowercase();
1007
1008        match value_lower.as_str() {
1009            "1" | "yes" | "true" | "on" => Some(true),
1010            "0" | "no" | "false" | "off" => Some(false),
1011            _ => None,
1012        }
1013    }
1014
1015    /// Format a boolean value for use in systemd unit files
1016    ///
1017    /// This converts a boolean to the canonical systemd format:
1018    /// - `true` becomes `"yes"`
1019    /// - `false` becomes `"no"`
1020    ///
1021    /// # Example
1022    ///
1023    /// ```
1024    /// # use systemd_unit_edit::Entry;
1025    /// assert_eq!(Entry::format_bool(true), "yes");
1026    /// assert_eq!(Entry::format_bool(false), "no");
1027    /// ```
1028    pub fn format_bool(value: bool) -> &'static str {
1029        if value {
1030            "yes"
1031        } else {
1032            "no"
1033        }
1034    }
1035
1036    /// Expand systemd specifiers in the value
1037    ///
1038    /// This replaces systemd specifiers like `%i`, `%u`, `%h` with their
1039    /// values from the provided context.
1040    ///
1041    /// # Example
1042    ///
1043    /// ```
1044    /// # use systemd_unit_edit::{SystemdUnit, SpecifierContext};
1045    /// # use std::str::FromStr;
1046    /// let unit = SystemdUnit::from_str("[Service]\nWorkingDirectory=/var/lib/%i\n").unwrap();
1047    /// let section = unit.get_section("Service").unwrap();
1048    /// let entry = section.entries().next().unwrap();
1049    ///
1050    /// let mut ctx = SpecifierContext::new();
1051    /// ctx.set("i", "myinstance");
1052    ///
1053    /// assert_eq!(entry.expand_specifiers(&ctx), Some("/var/lib/myinstance".to_string()));
1054    /// ```
1055    pub fn expand_specifiers(
1056        &self,
1057        context: &crate::specifier::SpecifierContext,
1058    ) -> Option<String> {
1059        let value = self.value()?;
1060        Some(context.expand(&value))
1061    }
1062
1063    /// Get the raw syntax node
1064    pub fn syntax(&self) -> &SyntaxNode {
1065        &self.0
1066    }
1067}
1068
1069impl AstNode for Entry {
1070    type Language = Lang;
1071
1072    fn can_cast(kind: SyntaxKind) -> bool {
1073        kind == SyntaxKind::ENTRY
1074    }
1075
1076    fn cast(node: SyntaxNode) -> Option<Self> {
1077        if node.kind() == SyntaxKind::ENTRY {
1078            Some(Entry(node))
1079        } else {
1080            None
1081        }
1082    }
1083
1084    fn syntax(&self) -> &SyntaxNode {
1085        &self.0
1086    }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use super::*;
1092
1093    #[test]
1094    fn test_parse_simple() {
1095        let input = r#"[Unit]
1096Description=Test Service
1097After=network.target
1098"#;
1099        let unit = SystemdUnit::from_str(input).unwrap();
1100        assert_eq!(unit.sections().count(), 1);
1101
1102        let section = unit.sections().next().unwrap();
1103        assert_eq!(section.name(), Some("Unit".to_string()));
1104        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1105        assert_eq!(section.get("After"), Some("network.target".to_string()));
1106    }
1107
1108    #[test]
1109    fn test_parse_with_comments() {
1110        let input = r#"# Top comment
1111[Unit]
1112# Comment before description
1113Description=Test Service
1114; Semicolon comment
1115After=network.target
1116"#;
1117        let unit = SystemdUnit::from_str(input).unwrap();
1118        assert_eq!(unit.sections().count(), 1);
1119
1120        let section = unit.sections().next().unwrap();
1121        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1122    }
1123
1124    #[test]
1125    fn test_parse_multiple_sections() {
1126        let input = r#"[Unit]
1127Description=Test Service
1128
1129[Service]
1130Type=simple
1131ExecStart=/usr/bin/test
1132
1133[Install]
1134WantedBy=multi-user.target
1135"#;
1136        let unit = SystemdUnit::from_str(input).unwrap();
1137        assert_eq!(unit.sections().count(), 3);
1138
1139        let unit_section = unit.get_section("Unit").unwrap();
1140        assert_eq!(
1141            unit_section.get("Description"),
1142            Some("Test Service".to_string())
1143        );
1144
1145        let service_section = unit.get_section("Service").unwrap();
1146        assert_eq!(service_section.get("Type"), Some("simple".to_string()));
1147        assert_eq!(
1148            service_section.get("ExecStart"),
1149            Some("/usr/bin/test".to_string())
1150        );
1151
1152        let install_section = unit.get_section("Install").unwrap();
1153        assert_eq!(
1154            install_section.get("WantedBy"),
1155            Some("multi-user.target".to_string())
1156        );
1157    }
1158
1159    #[test]
1160    fn test_parse_with_spaces() {
1161        let input = "[Unit]\nDescription = Test Service\n";
1162        let unit = SystemdUnit::from_str(input).unwrap();
1163
1164        let section = unit.sections().next().unwrap();
1165        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1166    }
1167
1168    #[test]
1169    fn test_line_continuation() {
1170        let input = "[Service]\nExecStart=/bin/echo \\\n  hello world\n";
1171        let unit = SystemdUnit::from_str(input).unwrap();
1172
1173        let section = unit.sections().next().unwrap();
1174        let entry = section.entries().next().unwrap();
1175        assert_eq!(entry.key(), Some("ExecStart".to_string()));
1176        // Line continuation: backslash is replaced with space
1177        assert_eq!(entry.value(), Some("/bin/echo   hello world".to_string()));
1178    }
1179
1180    #[test]
1181    fn test_lossless_roundtrip() {
1182        let input = r#"# Comment
1183[Unit]
1184Description=Test Service
1185After=network.target
1186
1187[Service]
1188Type=simple
1189ExecStart=/usr/bin/test
1190"#;
1191        let unit = SystemdUnit::from_str(input).unwrap();
1192        let output = unit.text();
1193        assert_eq!(input, output);
1194    }
1195
1196    #[test]
1197    fn test_set_value() {
1198        let input = r#"[Unit]
1199Description=Test Service
1200"#;
1201        let unit = SystemdUnit::from_str(input).unwrap();
1202        {
1203            let mut section = unit.sections().next().unwrap();
1204            section.set("Description", "Updated Service");
1205        }
1206
1207        let section = unit.sections().next().unwrap();
1208        assert_eq!(
1209            section.get("Description"),
1210            Some("Updated Service".to_string())
1211        );
1212    }
1213
1214    #[test]
1215    fn test_add_new_entry() {
1216        let input = r#"[Unit]
1217Description=Test Service
1218"#;
1219        let unit = SystemdUnit::from_str(input).unwrap();
1220        {
1221            let mut section = unit.sections().next().unwrap();
1222            section.set("After", "network.target");
1223        }
1224
1225        let section = unit.sections().next().unwrap();
1226        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1227        assert_eq!(section.get("After"), Some("network.target".to_string()));
1228    }
1229
1230    #[test]
1231    fn test_multiple_values_same_key() {
1232        let input = r#"[Unit]
1233Wants=foo.service
1234Wants=bar.service
1235"#;
1236        let unit = SystemdUnit::from_str(input).unwrap();
1237        let section = unit.sections().next().unwrap();
1238
1239        // get() returns the first value
1240        assert_eq!(section.get("Wants"), Some("foo.service".to_string()));
1241
1242        // get_all() returns all values
1243        let all_wants = section.get_all("Wants");
1244        assert_eq!(all_wants.len(), 2);
1245        assert_eq!(all_wants[0], "foo.service");
1246        assert_eq!(all_wants[1], "bar.service");
1247    }
1248
1249    #[test]
1250    fn test_add_multiple_entries() {
1251        let input = r#"[Unit]
1252Description=Test Service
1253"#;
1254        let unit = SystemdUnit::from_str(input).unwrap();
1255        {
1256            let mut section = unit.sections().next().unwrap();
1257            section.add("Wants", "foo.service");
1258            section.add("Wants", "bar.service");
1259        }
1260
1261        let section = unit.sections().next().unwrap();
1262        let all_wants = section.get_all("Wants");
1263        assert_eq!(all_wants.len(), 2);
1264        assert_eq!(all_wants[0], "foo.service");
1265        assert_eq!(all_wants[1], "bar.service");
1266    }
1267
1268    #[test]
1269    fn test_remove_entry() {
1270        let input = r#"[Unit]
1271Description=Test Service
1272After=network.target
1273"#;
1274        let unit = SystemdUnit::from_str(input).unwrap();
1275        {
1276            let mut section = unit.sections().next().unwrap();
1277            section.remove("After");
1278        }
1279
1280        let section = unit.sections().next().unwrap();
1281        assert_eq!(section.get("Description"), Some("Test Service".to_string()));
1282        assert_eq!(section.get("After"), None);
1283    }
1284
1285    #[test]
1286    fn test_remove_all_entries() {
1287        let input = r#"[Unit]
1288Wants=foo.service
1289Wants=bar.service
1290Description=Test
1291"#;
1292        let unit = SystemdUnit::from_str(input).unwrap();
1293        {
1294            let mut section = unit.sections().next().unwrap();
1295            section.remove_all("Wants");
1296        }
1297
1298        let section = unit.sections().next().unwrap();
1299        assert_eq!(section.get_all("Wants").len(), 0);
1300        assert_eq!(section.get("Description"), Some("Test".to_string()));
1301    }
1302
1303    #[test]
1304    fn test_unescape_basic() {
1305        let input = r#"[Unit]
1306Description=Test\nService
1307"#;
1308        let unit = SystemdUnit::from_str(input).unwrap();
1309        let section = unit.sections().next().unwrap();
1310        let entry = section.entries().next().unwrap();
1311
1312        assert_eq!(entry.value(), Some("Test\\nService".to_string()));
1313        assert_eq!(entry.unescape_value(), Some("Test\nService".to_string()));
1314    }
1315
1316    #[test]
1317    fn test_unescape_all_escapes() {
1318        let input = r#"[Unit]
1319Value=\n\t\r\\\"\'\x41\101\u0041\U00000041
1320"#;
1321        let unit = SystemdUnit::from_str(input).unwrap();
1322        let section = unit.sections().next().unwrap();
1323        let entry = section.entries().next().unwrap();
1324
1325        let unescaped = entry.unescape_value().unwrap();
1326        // \n = newline, \t = tab, \r = carriage return, \\ = backslash
1327        // \" = quote, \' = single quote
1328        // \x41 = 'A', \101 = 'A', \u0041 = 'A', \U00000041 = 'A'
1329        assert_eq!(unescaped, "\n\t\r\\\"'AAAA");
1330    }
1331
1332    #[test]
1333    fn test_unescape_unicode() {
1334        let input = r#"[Unit]
1335Value=Hello\u0020World\U0001F44D
1336"#;
1337        let unit = SystemdUnit::from_str(input).unwrap();
1338        let section = unit.sections().next().unwrap();
1339        let entry = section.entries().next().unwrap();
1340
1341        let unescaped = entry.unescape_value().unwrap();
1342        // \u0020 = space, \U0001F44D = ๐Ÿ‘
1343        assert_eq!(unescaped, "Hello World๐Ÿ‘");
1344    }
1345
1346    #[test]
1347    fn test_escape_value() {
1348        let text = "Hello\nWorld\t\"Test\"\\Path";
1349        let escaped = Entry::escape_value(text);
1350        assert_eq!(escaped, "Hello\\nWorld\\t\\\"Test\\\"\\\\Path");
1351    }
1352
1353    #[test]
1354    fn test_escape_unescape_roundtrip() {
1355        let original = "Test\nwith\ttabs\rand\"quotes\"\\backslash";
1356        let escaped = Entry::escape_value(original);
1357        let unescaped = unescape_string(&escaped);
1358        assert_eq!(original, unescaped);
1359    }
1360
1361    #[test]
1362    fn test_unescape_invalid_sequences() {
1363        // Invalid escape sequences should be kept as-is or handled gracefully
1364        let input = r#"[Unit]
1365Value=\z\xFF\u12\U1234
1366"#;
1367        let unit = SystemdUnit::from_str(input).unwrap();
1368        let section = unit.sections().next().unwrap();
1369        let entry = section.entries().next().unwrap();
1370
1371        let unescaped = entry.unescape_value().unwrap();
1372        // \z is unknown, \xFF has only 2 chars but needs hex, \u12 and \U1234 are incomplete
1373        assert!(unescaped.contains("\\z"));
1374    }
1375
1376    #[test]
1377    fn test_quoted_double_quotes() {
1378        let input = r#"[Unit]
1379Description="Test Service"
1380"#;
1381        let unit = SystemdUnit::from_str(input).unwrap();
1382        let section = unit.sections().next().unwrap();
1383        let entry = section.entries().next().unwrap();
1384
1385        assert_eq!(entry.value(), Some("\"Test Service\"".to_string()));
1386        assert_eq!(entry.quoted_value(), Some("\"Test Service\"".to_string()));
1387        assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1388        assert_eq!(entry.is_quoted(), Some('"'));
1389    }
1390
1391    #[test]
1392    fn test_quoted_single_quotes() {
1393        let input = r#"[Unit]
1394Description='Test Service'
1395"#;
1396        let unit = SystemdUnit::from_str(input).unwrap();
1397        let section = unit.sections().next().unwrap();
1398        let entry = section.entries().next().unwrap();
1399
1400        assert_eq!(entry.value(), Some("'Test Service'".to_string()));
1401        assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1402        assert_eq!(entry.is_quoted(), Some('\''));
1403    }
1404
1405    #[test]
1406    fn test_quoted_with_whitespace() {
1407        let input = r#"[Unit]
1408Description="  Test Service  "
1409"#;
1410        let unit = SystemdUnit::from_str(input).unwrap();
1411        let section = unit.sections().next().unwrap();
1412        let entry = section.entries().next().unwrap();
1413
1414        // Quotes preserve internal whitespace
1415        assert_eq!(entry.unquoted_value(), Some("  Test Service  ".to_string()));
1416    }
1417
1418    #[test]
1419    fn test_unquoted_value() {
1420        let input = r#"[Unit]
1421Description=Test Service
1422"#;
1423        let unit = SystemdUnit::from_str(input).unwrap();
1424        let section = unit.sections().next().unwrap();
1425        let entry = section.entries().next().unwrap();
1426
1427        assert_eq!(entry.value(), Some("Test Service".to_string()));
1428        assert_eq!(entry.unquoted_value(), Some("Test Service".to_string()));
1429        assert_eq!(entry.is_quoted(), None);
1430    }
1431
1432    #[test]
1433    fn test_mismatched_quotes() {
1434        let input = r#"[Unit]
1435Description="Test Service'
1436"#;
1437        let unit = SystemdUnit::from_str(input).unwrap();
1438        let section = unit.sections().next().unwrap();
1439        let entry = section.entries().next().unwrap();
1440
1441        // Mismatched quotes should not be considered quoted
1442        assert_eq!(entry.is_quoted(), None);
1443        assert_eq!(entry.unquoted_value(), Some("\"Test Service'".to_string()));
1444    }
1445
1446    #[test]
1447    fn test_empty_quotes() {
1448        let input = r#"[Unit]
1449Description=""
1450"#;
1451        let unit = SystemdUnit::from_str(input).unwrap();
1452        let section = unit.sections().next().unwrap();
1453        let entry = section.entries().next().unwrap();
1454
1455        assert_eq!(entry.is_quoted(), Some('"'));
1456        assert_eq!(entry.unquoted_value(), Some("".to_string()));
1457    }
1458
1459    #[test]
1460    fn test_value_as_list() {
1461        let input = r#"[Unit]
1462After=network.target remote-fs.target
1463"#;
1464        let unit = SystemdUnit::from_str(input).unwrap();
1465        let section = unit.sections().next().unwrap();
1466        let entry = section.entries().next().unwrap();
1467
1468        let list = entry.value_as_list();
1469        assert_eq!(list.len(), 2);
1470        assert_eq!(list[0], "network.target");
1471        assert_eq!(list[1], "remote-fs.target");
1472    }
1473
1474    #[test]
1475    fn test_value_as_list_single() {
1476        let input = r#"[Unit]
1477After=network.target
1478"#;
1479        let unit = SystemdUnit::from_str(input).unwrap();
1480        let section = unit.sections().next().unwrap();
1481        let entry = section.entries().next().unwrap();
1482
1483        let list = entry.value_as_list();
1484        assert_eq!(list.len(), 1);
1485        assert_eq!(list[0], "network.target");
1486    }
1487
1488    #[test]
1489    fn test_value_as_list_empty() {
1490        let input = r#"[Unit]
1491After=
1492"#;
1493        let unit = SystemdUnit::from_str(input).unwrap();
1494        let section = unit.sections().next().unwrap();
1495        let entry = section.entries().next().unwrap();
1496
1497        let list = entry.value_as_list();
1498        assert_eq!(list.len(), 0);
1499    }
1500
1501    #[test]
1502    fn test_value_as_list_with_extra_whitespace() {
1503        let input = r#"[Unit]
1504After=  network.target   remote-fs.target
1505"#;
1506        let unit = SystemdUnit::from_str(input).unwrap();
1507        let section = unit.sections().next().unwrap();
1508        let entry = section.entries().next().unwrap();
1509
1510        let list = entry.value_as_list();
1511        assert_eq!(list.len(), 2);
1512        assert_eq!(list[0], "network.target");
1513        assert_eq!(list[1], "remote-fs.target");
1514    }
1515
1516    #[test]
1517    fn test_section_get_list() {
1518        let input = r#"[Unit]
1519After=network.target remote-fs.target
1520"#;
1521        let unit = SystemdUnit::from_str(input).unwrap();
1522        let section = unit.sections().next().unwrap();
1523
1524        let list = section.get_list("After");
1525        assert_eq!(list.len(), 2);
1526        assert_eq!(list[0], "network.target");
1527        assert_eq!(list[1], "remote-fs.target");
1528    }
1529
1530    #[test]
1531    fn test_section_get_list_missing() {
1532        let input = r#"[Unit]
1533Description=Test
1534"#;
1535        let unit = SystemdUnit::from_str(input).unwrap();
1536        let section = unit.sections().next().unwrap();
1537
1538        let list = section.get_list("After");
1539        assert_eq!(list.len(), 0);
1540    }
1541
1542    #[test]
1543    fn test_section_set_list() {
1544        let input = r#"[Unit]
1545Description=Test
1546"#;
1547        let unit = SystemdUnit::from_str(input).unwrap();
1548        {
1549            let mut section = unit.sections().next().unwrap();
1550            section.set_list("After", &["network.target", "remote-fs.target"]);
1551        }
1552
1553        let section = unit.sections().next().unwrap();
1554        let list = section.get_list("After");
1555        assert_eq!(list.len(), 2);
1556        assert_eq!(list[0], "network.target");
1557        assert_eq!(list[1], "remote-fs.target");
1558    }
1559
1560    #[test]
1561    fn test_section_set_list_replaces() {
1562        let input = r#"[Unit]
1563After=foo.target
1564"#;
1565        let unit = SystemdUnit::from_str(input).unwrap();
1566        {
1567            let mut section = unit.sections().next().unwrap();
1568            section.set_list("After", &["network.target", "remote-fs.target"]);
1569        }
1570
1571        let section = unit.sections().next().unwrap();
1572        let list = section.get_list("After");
1573        assert_eq!(list.len(), 2);
1574        assert_eq!(list[0], "network.target");
1575        assert_eq!(list[1], "remote-fs.target");
1576    }
1577
1578    #[test]
1579    fn test_value_as_bool_positive() {
1580        let inputs = vec!["yes", "true", "1", "on", "YES", "True", "ON"];
1581
1582        for input_val in inputs {
1583            let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1584            let unit = SystemdUnit::from_str(&input).unwrap();
1585            let section = unit.sections().next().unwrap();
1586            let entry = section.entries().next().unwrap();
1587            assert_eq!(
1588                entry.value_as_bool(),
1589                Some(true),
1590                "Failed for input: {}",
1591                input_val
1592            );
1593        }
1594    }
1595
1596    #[test]
1597    fn test_value_as_bool_negative() {
1598        let inputs = vec!["no", "false", "0", "off", "NO", "False", "OFF"];
1599
1600        for input_val in inputs {
1601            let input = format!("[Service]\nRemainAfterExit={}\n", input_val);
1602            let unit = SystemdUnit::from_str(&input).unwrap();
1603            let section = unit.sections().next().unwrap();
1604            let entry = section.entries().next().unwrap();
1605            assert_eq!(
1606                entry.value_as_bool(),
1607                Some(false),
1608                "Failed for input: {}",
1609                input_val
1610            );
1611        }
1612    }
1613
1614    #[test]
1615    fn test_value_as_bool_invalid() {
1616        let input = r#"[Service]
1617RemainAfterExit=maybe
1618"#;
1619        let unit = SystemdUnit::from_str(input).unwrap();
1620        let section = unit.sections().next().unwrap();
1621        let entry = section.entries().next().unwrap();
1622        assert_eq!(entry.value_as_bool(), None);
1623    }
1624
1625    #[test]
1626    fn test_value_as_bool_with_whitespace() {
1627        let input = r#"[Service]
1628RemainAfterExit=  yes
1629"#;
1630        let unit = SystemdUnit::from_str(input).unwrap();
1631        let section = unit.sections().next().unwrap();
1632        let entry = section.entries().next().unwrap();
1633        assert_eq!(entry.value_as_bool(), Some(true));
1634    }
1635
1636    #[test]
1637    fn test_format_bool() {
1638        assert_eq!(Entry::format_bool(true), "yes");
1639        assert_eq!(Entry::format_bool(false), "no");
1640    }
1641
1642    #[test]
1643    fn test_section_get_bool() {
1644        let input = r#"[Service]
1645RemainAfterExit=yes
1646Type=simple
1647"#;
1648        let unit = SystemdUnit::from_str(input).unwrap();
1649        let section = unit.sections().next().unwrap();
1650
1651        assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1652        assert_eq!(section.get_bool("Type"), None); // Not a boolean
1653        assert_eq!(section.get_bool("Missing"), None); // Doesn't exist
1654    }
1655
1656    #[test]
1657    fn test_section_set_bool() {
1658        let input = r#"[Service]
1659Type=simple
1660"#;
1661        let unit = SystemdUnit::from_str(input).unwrap();
1662        {
1663            let mut section = unit.sections().next().unwrap();
1664            section.set_bool("RemainAfterExit", true);
1665            section.set_bool("PrivateTmp", false);
1666        }
1667
1668        let section = unit.sections().next().unwrap();
1669        assert_eq!(section.get("RemainAfterExit"), Some("yes".to_string()));
1670        assert_eq!(section.get("PrivateTmp"), Some("no".to_string()));
1671        assert_eq!(section.get_bool("RemainAfterExit"), Some(true));
1672        assert_eq!(section.get_bool("PrivateTmp"), Some(false));
1673    }
1674}