Skip to main content

deb822_lossless/
lossless.rs

1//! Parser for deb822 style files.
2//!
3//! This parser can be used to parse files in the deb822 format, while preserving
4//! all whitespace and comments. It is based on the [rowan] library, which is a
5//! lossless parser library for Rust.
6//!
7//! Once parsed, the file can be traversed or modified, and then written back to
8//! a file.
9//!
10//! # Example
11//!
12//! ```rust
13//! use deb822_lossless::Deb822;
14//! use std::str::FromStr;
15//!
16//! let input = r###"Package: deb822-lossless
17//! ## Comments are preserved
18//! Maintainer: Jelmer Vernooij <jelmer@debian.org>
19//! Homepage: https://github.com/jelmer/deb822-lossless
20//! Section: rust
21//!
22//! Package: deb822-lossless
23//! Architecture: any
24//! Description: Lossless parser for deb822 style files.
25//!   This parser can be used to parse files in the deb822 format, while preserving
26//!   all whitespace and comments. It is based on the [rowan] library, which is a
27//!   lossless parser library for Rust.
28//! "###;
29//!
30//! let deb822 = Deb822::from_str(input).unwrap();
31//! assert_eq!(deb822.paragraphs().count(), 2);
32//! let homepage = deb822.paragraphs().nth(0).unwrap().get("Homepage");
33//! assert_eq!(homepage.as_deref(), Some("https://github.com/jelmer/deb822-lossless"));
34//! ```
35
36use crate::{
37    lex::lex,
38    lex::SyntaxKind::{self, *},
39    Indentation,
40};
41use rowan::ast::AstNode;
42use std::path::Path;
43use std::str::FromStr;
44
45/// A positioned parse error containing location information.
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct PositionedParseError {
48    /// The error message
49    pub message: String,
50    /// The text range where the error occurred
51    pub range: rowan::TextRange,
52    /// Optional error code for categorization
53    pub code: Option<String>,
54}
55
56impl std::fmt::Display for PositionedParseError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.message)
59    }
60}
61
62impl std::error::Error for PositionedParseError {}
63
64/// List of encountered syntax errors.
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub struct ParseError(pub Vec<String>);
67
68impl std::fmt::Display for ParseError {
69    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
70        for err in &self.0 {
71            writeln!(f, "{}", err)?;
72        }
73        Ok(())
74    }
75}
76
77impl std::error::Error for ParseError {}
78
79/// Error parsing deb822 control files
80#[derive(Debug)]
81pub enum Error {
82    /// A syntax error was encountered while parsing the file.
83    ParseError(ParseError),
84
85    /// An I/O error was encountered while reading the file.
86    IoError(std::io::Error),
87
88    /// An invalid value was provided (e.g., empty continuation lines).
89    InvalidValue(String),
90}
91
92impl std::fmt::Display for Error {
93    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
94        match &self {
95            Error::ParseError(err) => write!(f, "{}", err),
96            Error::IoError(err) => write!(f, "{}", err),
97            Error::InvalidValue(msg) => write!(f, "Invalid value: {}", msg),
98        }
99    }
100}
101
102impl From<ParseError> for Error {
103    fn from(err: ParseError) -> Self {
104        Self::ParseError(err)
105    }
106}
107
108impl From<std::io::Error> for Error {
109    fn from(err: std::io::Error) -> Self {
110        Self::IoError(err)
111    }
112}
113
114impl std::error::Error for Error {}
115
116/// Second, implementing the `Language` trait teaches rowan to convert between
117/// these two SyntaxKind types, allowing for a nicer SyntaxNode API where
118/// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub enum Lang {}
121impl rowan::Language for Lang {
122    type Kind = SyntaxKind;
123    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
124        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
125    }
126    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
127        kind.into()
128    }
129}
130
131/// GreenNode is an immutable tree, which is cheap to change,
132/// but doesn't contain offsets and parent pointers.
133use rowan::GreenNode;
134
135/// You can construct GreenNodes by hand, but a builder
136/// is helpful for top-down parsers: it maintains a stack
137/// of currently in-progress nodes
138use rowan::GreenNodeBuilder;
139
140/// The parse results are stored as a "green tree".
141/// We'll discuss working with the results later
142pub(crate) struct Parse {
143    pub(crate) green_node: GreenNode,
144    #[allow(unused)]
145    pub(crate) errors: Vec<String>,
146    pub(crate) positioned_errors: Vec<PositionedParseError>,
147}
148
149pub(crate) fn parse(text: &str) -> Parse {
150    struct Parser<'a> {
151        /// input tokens, including whitespace,
152        /// in *reverse* order.
153        tokens: Vec<(SyntaxKind, &'a str)>,
154        /// the in-progress tree.
155        builder: GreenNodeBuilder<'static>,
156        /// the list of syntax errors we've accumulated
157        /// so far.
158        errors: Vec<String>,
159        /// positioned errors with location information
160        positioned_errors: Vec<PositionedParseError>,
161        /// All tokens with their positions in forward order for position tracking
162        token_positions: Vec<(SyntaxKind, rowan::TextSize, rowan::TextSize)>,
163        /// current token index (counting from the end since tokens are in reverse)
164        current_token_index: usize,
165    }
166
167    impl<'a> Parser<'a> {
168        /// Skip to next paragraph boundary for error recovery
169        fn skip_to_paragraph_boundary(&mut self) {
170            while self.current().is_some() {
171                match self.current() {
172                    Some(NEWLINE) => {
173                        self.bump();
174                        // Check if next line starts a new paragraph (key at start of line)
175                        if self.at_paragraph_start() {
176                            break;
177                        }
178                    }
179                    _ => {
180                        self.bump();
181                    }
182                }
183            }
184        }
185
186        /// Check if we're at the start of a new paragraph
187        fn at_paragraph_start(&self) -> bool {
188            match self.current() {
189                Some(KEY) => true,
190                Some(COMMENT) => true,
191                None => true, // EOF is a valid paragraph boundary
192                _ => false,
193            }
194        }
195
196        /// Attempt to recover from entry parsing errors
197        fn recover_entry(&mut self) {
198            // Skip to end of current line
199            while self.current().is_some() && self.current() != Some(NEWLINE) {
200                self.bump();
201            }
202            // Consume the newline if present
203            if self.current() == Some(NEWLINE) {
204                self.bump();
205            }
206        }
207        fn parse_entry(&mut self) {
208            // Handle leading comments
209            while self.current() == Some(COMMENT) {
210                self.bump();
211
212                match self.current() {
213                    Some(NEWLINE) => {
214                        self.bump();
215                    }
216                    None => {
217                        return;
218                    }
219                    Some(g) => {
220                        self.builder.start_node(ERROR.into());
221                        self.add_positioned_error(
222                            format!("expected newline after comment, got {g:?}"),
223                            Some("unexpected_token_after_comment".to_string()),
224                        );
225                        self.bump();
226                        self.builder.finish_node();
227                        self.recover_entry();
228                        return;
229                    }
230                }
231            }
232
233            self.builder.start_node(ENTRY.into());
234            let mut entry_has_errors = false;
235
236            // Parse the key
237            if self.current() == Some(KEY) {
238                self.bump();
239                self.skip_ws();
240            } else {
241                entry_has_errors = true;
242                self.builder.start_node(ERROR.into());
243
244                // Enhanced error recovery for malformed keys
245                match self.current() {
246                    Some(VALUE) | Some(WHITESPACE) => {
247                        self.add_positioned_error(
248                            "field name cannot start with whitespace or special characters"
249                                .to_string(),
250                            Some("invalid_field_name".to_string()),
251                        );
252                        // Try to consume what might be an intended key
253                        while self.current() == Some(VALUE) || self.current() == Some(WHITESPACE) {
254                            self.bump();
255                        }
256                    }
257                    Some(COLON) => {
258                        self.add_positioned_error(
259                            "field name missing before colon".to_string(),
260                            Some("missing_field_name".to_string()),
261                        );
262                    }
263                    Some(NEWLINE) => {
264                        self.add_positioned_error(
265                            "empty line where field expected".to_string(),
266                            Some("empty_field_line".to_string()),
267                        );
268                        self.builder.finish_node();
269                        self.builder.finish_node();
270                        return;
271                    }
272                    _ => {
273                        self.add_positioned_error(
274                            format!("expected field name, got {:?}", self.current()),
275                            Some("missing_key".to_string()),
276                        );
277                        if self.current().is_some() {
278                            self.bump();
279                        }
280                    }
281                }
282                self.builder.finish_node();
283            }
284
285            // Parse the colon
286            if self.current() == Some(COLON) {
287                self.bump();
288                self.skip_ws();
289            } else {
290                entry_has_errors = true;
291                self.builder.start_node(ERROR.into());
292
293                // Enhanced error recovery for missing colon
294                match self.current() {
295                    Some(VALUE) => {
296                        self.add_positioned_error(
297                            "missing colon ':' after field name".to_string(),
298                            Some("missing_colon".to_string()),
299                        );
300                        // Don't consume the value, let it be parsed as the field value
301                    }
302                    Some(NEWLINE) => {
303                        self.add_positioned_error(
304                            "field name without value (missing colon and value)".to_string(),
305                            Some("incomplete_field".to_string()),
306                        );
307                        self.builder.finish_node();
308                        self.builder.finish_node();
309                        return;
310                    }
311                    Some(KEY) => {
312                        self.add_positioned_error(
313                            "field name followed by another field name (missing colon and value)"
314                                .to_string(),
315                            Some("consecutive_field_names".to_string()),
316                        );
317                        // Don't consume the next key, let it be parsed as a new entry
318                        self.builder.finish_node();
319                        self.builder.finish_node();
320                        return;
321                    }
322                    _ => {
323                        self.add_positioned_error(
324                            format!("expected colon ':', got {:?}", self.current()),
325                            Some("missing_colon".to_string()),
326                        );
327                        if self.current().is_some() {
328                            self.bump();
329                        }
330                    }
331                }
332                self.builder.finish_node();
333            }
334
335            // Parse the value (potentially multi-line)
336            loop {
337                while self.current() == Some(WHITESPACE) || self.current() == Some(VALUE) {
338                    self.bump();
339                }
340
341                match self.current() {
342                    None => {
343                        break;
344                    }
345                    Some(NEWLINE) => {
346                        self.bump();
347                    }
348                    Some(KEY) => {
349                        // We've hit another field, this entry is complete
350                        break;
351                    }
352                    Some(g) => {
353                        self.builder.start_node(ERROR.into());
354                        self.add_positioned_error(
355                            format!("unexpected token in field value: {g:?}"),
356                            Some("unexpected_value_token".to_string()),
357                        );
358                        self.bump();
359                        self.builder.finish_node();
360                    }
361                }
362
363                // Check for continuation lines
364                if self.current() == Some(INDENT) {
365                    self.bump();
366                    self.skip_ws();
367
368                    // After indent and whitespace, we must have actual content (VALUE token)
369                    // An empty continuation line (indent followed immediately by newline or EOF)
370                    // is not valid according to Debian Policy
371                    if self.current() == Some(NEWLINE) || self.current().is_none() {
372                        self.builder.start_node(ERROR.into());
373                        self.add_positioned_error(
374                            "empty continuation line (line with only whitespace)".to_string(),
375                            Some("empty_continuation_line".to_string()),
376                        );
377                        self.builder.finish_node();
378                        break;
379                    }
380                } else {
381                    break;
382                }
383            }
384
385            self.builder.finish_node();
386
387            // If the entry had errors, we might want to recover
388            if entry_has_errors && !self.at_paragraph_start() && self.current().is_some() {
389                self.recover_entry();
390            }
391        }
392
393        fn parse_paragraph(&mut self) {
394            self.builder.start_node(PARAGRAPH.into());
395
396            let mut consecutive_errors = 0;
397            const MAX_CONSECUTIVE_ERRORS: usize = 5;
398
399            while self.current() != Some(NEWLINE) && self.current().is_some() {
400                let error_count_before = self.positioned_errors.len();
401
402                // Check if we're at a valid entry start
403                if self.current() == Some(KEY) || self.current() == Some(COMMENT) {
404                    self.parse_entry();
405
406                    // Reset consecutive error count if we successfully parsed something
407                    if self.positioned_errors.len() == error_count_before {
408                        consecutive_errors = 0;
409                    } else {
410                        consecutive_errors += 1;
411                    }
412                } else {
413                    // We're not at a valid entry start, this is an error
414                    consecutive_errors += 1;
415
416                    self.builder.start_node(ERROR.into());
417                    match self.current() {
418                        Some(VALUE) => {
419                            self.add_positioned_error(
420                                "orphaned text without field name".to_string(),
421                                Some("orphaned_text".to_string()),
422                            );
423                            // Consume the orphaned text
424                            while self.current() == Some(VALUE)
425                                || self.current() == Some(WHITESPACE)
426                            {
427                                self.bump();
428                            }
429                        }
430                        Some(COLON) => {
431                            self.add_positioned_error(
432                                "orphaned colon without field name".to_string(),
433                                Some("orphaned_colon".to_string()),
434                            );
435                            self.bump();
436                        }
437                        Some(INDENT) => {
438                            self.add_positioned_error(
439                                "unexpected indentation without field".to_string(),
440                                Some("unexpected_indent".to_string()),
441                            );
442                            self.bump();
443                        }
444                        _ => {
445                            self.add_positioned_error(
446                                format!(
447                                    "unexpected token at paragraph level: {:?}",
448                                    self.current()
449                                ),
450                                Some("unexpected_paragraph_token".to_string()),
451                            );
452                            self.bump();
453                        }
454                    }
455                    self.builder.finish_node();
456                }
457
458                // If we have too many consecutive errors, skip to paragraph boundary
459                if consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
460                    self.add_positioned_error(
461                        "too many consecutive parse errors, skipping to next paragraph".to_string(),
462                        Some("parse_recovery".to_string()),
463                    );
464                    self.skip_to_paragraph_boundary();
465                    break;
466                }
467            }
468
469            self.builder.finish_node();
470        }
471
472        fn parse(mut self) -> Parse {
473            // Make sure that the root node covers all source
474            self.builder.start_node(ROOT.into());
475            while self.current().is_some() {
476                self.skip_ws_and_newlines();
477                if self.current().is_some() {
478                    self.parse_paragraph();
479                }
480            }
481            // Don't forget to eat *trailing* whitespace
482            self.skip_ws_and_newlines();
483            // Close the root node.
484            self.builder.finish_node();
485
486            // Turn the builder into a GreenNode
487            Parse {
488                green_node: self.builder.finish(),
489                errors: self.errors,
490                positioned_errors: self.positioned_errors,
491            }
492        }
493        /// Advance one token, adding it to the current branch of the tree builder.
494        fn bump(&mut self) {
495            let (kind, text) = self.tokens.pop().unwrap();
496            self.builder.token(kind.into(), text);
497            if self.current_token_index > 0 {
498                self.current_token_index -= 1;
499            }
500        }
501        /// Peek at the first unprocessed token
502        fn current(&self) -> Option<SyntaxKind> {
503            self.tokens.last().map(|(kind, _)| *kind)
504        }
505
506        /// Add a positioned error at the current position
507        fn add_positioned_error(&mut self, message: String, code: Option<String>) {
508            let range = if self.current_token_index < self.token_positions.len() {
509                let (_, start, end) = self.token_positions[self.current_token_index];
510                rowan::TextRange::new(start, end)
511            } else {
512                // Default to end of text if no current token
513                let end = self
514                    .token_positions
515                    .last()
516                    .map(|(_, _, end)| *end)
517                    .unwrap_or_else(|| rowan::TextSize::from(0));
518                rowan::TextRange::new(end, end)
519            };
520
521            self.positioned_errors.push(PositionedParseError {
522                message: message.clone(),
523                range,
524                code,
525            });
526            self.errors.push(message);
527        }
528        fn skip_ws(&mut self) {
529            while self.current() == Some(WHITESPACE) || self.current() == Some(COMMENT) {
530                self.bump()
531            }
532        }
533        fn skip_ws_and_newlines(&mut self) {
534            while self.current() == Some(WHITESPACE)
535                || self.current() == Some(COMMENT)
536                || self.current() == Some(NEWLINE)
537            {
538                self.builder.start_node(EMPTY_LINE.into());
539                while self.current() != Some(NEWLINE) && self.current().is_some() {
540                    self.bump();
541                }
542                if self.current() == Some(NEWLINE) {
543                    self.bump();
544                }
545                self.builder.finish_node();
546            }
547        }
548    }
549
550    let mut tokens = lex(text).collect::<Vec<_>>();
551
552    // Build token positions in forward order
553    let mut token_positions = Vec::new();
554    let mut position = rowan::TextSize::from(0);
555    for (kind, text) in &tokens {
556        let start = position;
557        let end = start + rowan::TextSize::of(*text);
558        token_positions.push((*kind, start, end));
559        position = end;
560    }
561
562    // Reverse tokens for parsing (but keep positions in forward order)
563    tokens.reverse();
564    let current_token_index = tokens.len().saturating_sub(1);
565
566    Parser {
567        tokens,
568        builder: GreenNodeBuilder::new(),
569        errors: Vec::new(),
570        positioned_errors: Vec::new(),
571        token_positions,
572        current_token_index,
573    }
574    .parse()
575}
576
577/// To work with the parse results we need a view into the
578/// green tree - the Syntax tree.
579/// It is also immutable, like a GreenNode,
580/// but it contains parent pointers, offsets, and
581/// has identity semantics.
582type SyntaxNode = rowan::SyntaxNode<Lang>;
583#[allow(unused)]
584type SyntaxToken = rowan::SyntaxToken<Lang>;
585#[allow(unused)]
586type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
587
588impl Parse {
589    #[cfg(test)]
590    fn syntax(&self) -> SyntaxNode {
591        SyntaxNode::new_root(self.green_node.clone())
592    }
593
594    fn root_mut(&self) -> Deb822 {
595        Deb822::cast(SyntaxNode::new_root_mut(self.green_node.clone())).unwrap()
596    }
597}
598
599/// Calculate line and column (both 0-indexed) for the given offset in the tree.
600/// Column is measured in bytes from the start of the line.
601fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
602    let root = node.ancestors().last().unwrap_or_else(|| node.clone());
603    let mut line = 0;
604    let mut last_newline_offset = rowan::TextSize::from(0);
605
606    for element in root.preorder_with_tokens() {
607        if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
608            if token.text_range().start() >= offset {
609                break;
610            }
611
612            // Count newlines and track position of last one
613            for (idx, _) in token.text().match_indices('\n') {
614                line += 1;
615                last_newline_offset =
616                    token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
617            }
618        }
619    }
620
621    let column: usize = (offset - last_newline_offset).into();
622    (line, column)
623}
624
625macro_rules! ast_node {
626    ($ast:ident, $kind:ident) => {
627        #[doc = "An AST node representing a `"]
628        #[doc = stringify!($ast)]
629        #[doc = "`."]
630        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
631        #[repr(transparent)]
632        pub struct $ast(SyntaxNode);
633        impl $ast {
634            #[allow(unused)]
635            fn cast(node: SyntaxNode) -> Option<Self> {
636                if node.kind() == $kind {
637                    Some(Self(node))
638                } else {
639                    None
640                }
641            }
642
643            /// Get the line number (0-indexed) where this node starts.
644            pub fn line(&self) -> usize {
645                line_col_at_offset(&self.0, self.0.text_range().start()).0
646            }
647
648            /// Get the column number (0-indexed, in bytes) where this node starts.
649            pub fn column(&self) -> usize {
650                line_col_at_offset(&self.0, self.0.text_range().start()).1
651            }
652
653            /// Get both line and column (0-indexed) where this node starts.
654            /// Returns (line, column) where column is measured in bytes from the start of the line.
655            pub fn line_col(&self) -> (usize, usize) {
656                line_col_at_offset(&self.0, self.0.text_range().start())
657            }
658        }
659
660        impl AstNode for $ast {
661            type Language = Lang;
662
663            fn can_cast(kind: SyntaxKind) -> bool {
664                kind == $kind
665            }
666
667            fn cast(syntax: SyntaxNode) -> Option<Self> {
668                Self::cast(syntax)
669            }
670
671            fn syntax(&self) -> &SyntaxNode {
672                &self.0
673            }
674        }
675
676        impl std::fmt::Display for $ast {
677            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
678                write!(f, "{}", self.0.text())
679            }
680        }
681    };
682}
683
684ast_node!(Deb822, ROOT);
685ast_node!(Paragraph, PARAGRAPH);
686ast_node!(Entry, ENTRY);
687
688impl Default for Deb822 {
689    fn default() -> Self {
690        Self::new()
691    }
692}
693
694impl Deb822 {
695    /// Create an independent snapshot of this Deb822 file.
696    ///
697    /// This creates a new mutable tree that shares the same underlying immutable
698    /// GreenNode data. Modifications to the original will not affect the snapshot
699    /// and vice versa.
700    ///
701    /// This is more efficient than serializing and re-parsing because it reuses
702    /// the GreenNode structure.
703    ///
704    /// # Example
705    /// ```
706    /// use deb822_lossless::Deb822;
707    ///
708    /// let text = "Package: foo\n";
709    /// let deb822: Deb822 = text.parse().unwrap();
710    /// let snapshot = deb822.snapshot();
711    ///
712    /// // Modifications to deb822 won't affect snapshot
713    /// let mut para = deb822.paragraphs().next().unwrap();
714    /// para.set("Package", "modified");
715    ///
716    /// let snapshot_para = snapshot.paragraphs().next().unwrap();
717    /// assert_eq!(snapshot_para.get("Package").as_deref(), Some("foo"));
718    /// ```
719    pub fn snapshot(&self) -> Self {
720        Deb822(SyntaxNode::new_root_mut(self.0.green().into_owned()))
721    }
722
723    /// Create a new empty deb822 file.
724    pub fn new() -> Deb822 {
725        let mut builder = GreenNodeBuilder::new();
726
727        builder.start_node(ROOT.into());
728        builder.finish_node();
729        Deb822(SyntaxNode::new_root_mut(builder.finish()))
730    }
731
732    /// Parse deb822 text, returning a Parse result
733    pub fn parse(text: &str) -> crate::Parse<Deb822> {
734        crate::Parse::parse_deb822(text)
735    }
736
737    /// Provide a formatter that can handle indentation and trailing separators
738    ///
739    /// # Arguments
740    /// * `control` - The control file to format
741    /// * `indentation` - The indentation to use
742    /// * `immediate_empty_line` - Whether the value should always start with an empty line. If true,
743    ///   then the result becomes something like "Field:\n value". This parameter
744    ///   only applies to the values that will be formatted over more than one line.
745    /// * `max_line_length_one_liner` - If set, then this is the max length of the value
746    ///   if it is crammed into a "one-liner" value. If the value(s) fit into
747    ///   one line, this parameter will overrule immediate_empty_line.
748    /// * `sort_paragraphs` - If set, then this function will sort the paragraphs according to the
749    ///   given function.
750    /// * `sort_entries` - If set, then this function will sort the entries according to the
751    ///   given function.
752    #[must_use]
753    pub fn wrap_and_sort(
754        &self,
755        sort_paragraphs: Option<&dyn Fn(&Paragraph, &Paragraph) -> std::cmp::Ordering>,
756        wrap_and_sort_paragraph: Option<&dyn Fn(&Paragraph) -> Paragraph>,
757    ) -> Deb822 {
758        let mut builder = GreenNodeBuilder::new();
759        builder.start_node(ROOT.into());
760        let mut current = vec![];
761        let mut paragraphs = vec![];
762        for c in self.0.children_with_tokens() {
763            match c.kind() {
764                PARAGRAPH => {
765                    paragraphs.push((
766                        current,
767                        Paragraph::cast(c.as_node().unwrap().clone()).unwrap(),
768                    ));
769                    current = vec![];
770                }
771                COMMENT | ERROR => {
772                    current.push(c);
773                }
774                EMPTY_LINE => {
775                    current.extend(
776                        c.as_node()
777                            .unwrap()
778                            .children_with_tokens()
779                            .skip_while(|c| matches!(c.kind(), EMPTY_LINE | NEWLINE | WHITESPACE)),
780                    );
781                }
782                _ => {}
783            }
784        }
785        if let Some(sort_paragraph) = sort_paragraphs {
786            paragraphs.sort_by(|a, b| {
787                let a_key = &a.1;
788                let b_key = &b.1;
789                sort_paragraph(a_key, b_key)
790            });
791        }
792
793        for (i, paragraph) in paragraphs.into_iter().enumerate() {
794            if i > 0 {
795                builder.start_node(EMPTY_LINE.into());
796                builder.token(NEWLINE.into(), "\n");
797                builder.finish_node();
798            }
799            for c in paragraph.0.into_iter() {
800                builder.token(c.kind().into(), c.as_token().unwrap().text());
801            }
802            let new_paragraph = if let Some(ref ws) = wrap_and_sort_paragraph {
803                ws(&paragraph.1)
804            } else {
805                paragraph.1
806            };
807            inject(&mut builder, new_paragraph.0);
808        }
809
810        for c in current {
811            builder.token(c.kind().into(), c.as_token().unwrap().text());
812        }
813
814        builder.finish_node();
815        Self(SyntaxNode::new_root_mut(builder.finish()))
816    }
817
818    /// Normalize the spacing around field separators (colons) for all entries in all paragraphs in place.
819    ///
820    /// This ensures that there is exactly one space after the colon and before the value
821    /// for each field in every paragraph. This is a lossless operation that preserves the
822    /// field names, values, and comments, but normalizes the whitespace formatting.
823    ///
824    /// # Examples
825    ///
826    /// ```
827    /// use deb822_lossless::Deb822;
828    /// use std::str::FromStr;
829    ///
830    /// let input = "Field1:    value1\nField2:value2\n\nField3:  value3\n";
831    /// let mut deb822 = Deb822::from_str(input).unwrap();
832    ///
833    /// deb822.normalize_field_spacing();
834    /// assert_eq!(deb822.to_string(), "Field1: value1\nField2: value2\n\nField3: value3\n");
835    /// ```
836    pub fn normalize_field_spacing(&mut self) -> bool {
837        let mut any_changed = false;
838
839        // Collect paragraphs to avoid borrowing issues
840        let mut paragraphs: Vec<_> = self.paragraphs().collect();
841
842        // Normalize each paragraph
843        for para in &mut paragraphs {
844            if para.normalize_field_spacing() {
845                any_changed = true;
846            }
847        }
848
849        any_changed
850    }
851
852    /// Returns an iterator over all paragraphs in the file.
853    pub fn paragraphs(&self) -> impl Iterator<Item = Paragraph> {
854        self.0.children().filter_map(Paragraph::cast)
855    }
856
857    /// Converts the perceptual paragraph index to the node index.
858    fn convert_index(&self, index: usize) -> Option<usize> {
859        let mut current_pos = 0usize;
860        if index == 0 {
861            return Some(0);
862        }
863        for (i, node) in self.0.children_with_tokens().enumerate() {
864            if node.kind() == PARAGRAPH {
865                if current_pos == index {
866                    return Some(i);
867                }
868                current_pos += 1;
869            }
870        }
871
872        None
873    }
874
875    /// Delete trailing empty lines after specified node and before any non-empty line nodes.
876    fn delete_trailing_space(&self, start: usize) {
877        for (i, node) in self.0.children_with_tokens().enumerate() {
878            if i < start {
879                continue;
880            }
881            if node.kind() != EMPTY_LINE {
882                return;
883            }
884            // this is not a typo, the index will shift by one after deleting the node
885            // so instead of deleting using `i`, we use `start` as the start index
886            self.0.splice_children(start..start + 1, []);
887        }
888    }
889
890    /// Shared internal function to insert a new paragraph into the file.
891    fn insert_empty_paragraph(&mut self, index: Option<usize>) -> Paragraph {
892        let paragraph = Paragraph::new();
893        let mut to_insert = vec![];
894        if self.0.children().count() > 0 {
895            let mut builder = GreenNodeBuilder::new();
896            builder.start_node(EMPTY_LINE.into());
897            builder.token(NEWLINE.into(), "\n");
898            builder.finish_node();
899            to_insert.push(SyntaxNode::new_root_mut(builder.finish()).into());
900        }
901        to_insert.push(paragraph.0.clone().into());
902        let insertion_point = match index {
903            Some(i) => {
904                if to_insert.len() > 1 {
905                    to_insert.swap(0, 1);
906                }
907                i
908            }
909            None => self.0.children().count(),
910        };
911        self.0
912            .splice_children(insertion_point..insertion_point, to_insert);
913        paragraph
914    }
915
916    /// Insert a new empty paragraph into the file after specified index.
917    ///
918    /// # Examples
919    ///
920    /// ```
921    /// use deb822_lossless::{Deb822, Paragraph};
922    /// let mut d: Deb822 = vec![
923    ///     vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
924    ///     vec![("A", "B"), ("C", "D")].into_iter().collect(),
925    /// ]
926    /// .into_iter()
927    /// .collect();
928    /// let mut p = d.insert_paragraph(0);
929    /// p.set("Foo", "Baz");
930    /// assert_eq!(d.to_string(), "Foo: Baz\n\nFoo: Bar\nBaz: Qux\n\nA: B\nC: D\n");
931    /// let mut another = d.insert_paragraph(1);
932    /// another.set("Y", "Z");
933    /// assert_eq!(d.to_string(), "Foo: Baz\n\nY: Z\n\nFoo: Bar\nBaz: Qux\n\nA: B\nC: D\n");
934    /// ```
935    pub fn insert_paragraph(&mut self, index: usize) -> Paragraph {
936        self.insert_empty_paragraph(self.convert_index(index))
937    }
938
939    /// Remove the paragraph at the specified index from the file.
940    ///
941    /// # Examples
942    ///
943    /// ```
944    /// use deb822_lossless::Deb822;
945    /// let mut d: Deb822 = vec![
946    ///     vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
947    ///     vec![("A", "B"), ("C", "D")].into_iter().collect(),
948    /// ]
949    /// .into_iter()
950    /// .collect();
951    /// d.remove_paragraph(0);
952    /// assert_eq!(d.to_string(), "A: B\nC: D\n");
953    /// d.remove_paragraph(0);
954    /// assert_eq!(d.to_string(), "");
955    /// ```
956    pub fn remove_paragraph(&mut self, index: usize) {
957        if let Some(index) = self.convert_index(index) {
958            self.0.splice_children(index..index + 1, []);
959            self.delete_trailing_space(index);
960        }
961    }
962
963    /// Move a paragraph from one index to another.
964    ///
965    /// This moves the paragraph at `from_index` to `to_index`, shifting other paragraphs as needed.
966    /// If `from_index` equals `to_index`, no operation is performed.
967    ///
968    /// # Examples
969    ///
970    /// ```
971    /// use deb822_lossless::Deb822;
972    /// let mut d: Deb822 = vec![
973    ///     vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
974    ///     vec![("A", "B"), ("C", "D")].into_iter().collect(),
975    ///     vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
976    /// ]
977    /// .into_iter()
978    /// .collect();
979    /// d.move_paragraph(0, 2);
980    /// assert_eq!(d.to_string(), "A: B\nC: D\n\nX: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n");
981    /// ```
982    pub fn move_paragraph(&mut self, from_index: usize, to_index: usize) {
983        if from_index == to_index {
984            return;
985        }
986
987        // Get the paragraph count to validate indices
988        let paragraph_count = self.paragraphs().count();
989        if from_index >= paragraph_count || to_index >= paragraph_count {
990            return;
991        }
992
993        // Clone the paragraph node we want to move
994        let paragraph_to_move = self.paragraphs().nth(from_index).unwrap().0.clone();
995
996        // Remove the paragraph from its original position
997        let from_physical = self.convert_index(from_index).unwrap();
998
999        // Determine the range to remove (paragraph and possibly preceding EMPTY_LINE)
1000        let mut start_idx = from_physical;
1001        if from_physical > 0 {
1002            if let Some(prev_node) = self.0.children_with_tokens().nth(from_physical - 1) {
1003                if prev_node.kind() == EMPTY_LINE {
1004                    start_idx = from_physical - 1;
1005                }
1006            }
1007        }
1008
1009        // Remove the paragraph and any preceding EMPTY_LINE
1010        self.0.splice_children(start_idx..from_physical + 1, []);
1011        self.delete_trailing_space(start_idx);
1012
1013        // Calculate the physical insertion point
1014        // After removal, we need to determine where to insert
1015        // The semantics are: the moved paragraph ends up at logical index to_index in the final result
1016        let insert_at = if to_index > from_index {
1017            // Moving forward: after removal, to_index-1 paragraphs should be before the moved one
1018            // So we insert after paragraph at index (to_index - 1)
1019            let target_idx = to_index - 1;
1020            if let Some(target_physical) = self.convert_index(target_idx) {
1021                target_physical + 1
1022            } else {
1023                // If convert_index returns None, insert at the end
1024                self.0.children().count()
1025            }
1026        } else {
1027            // Moving backward: after removal, to_index paragraphs should be before the moved one
1028            // So we insert at paragraph index to_index
1029            if let Some(target_physical) = self.convert_index(to_index) {
1030                target_physical
1031            } else {
1032                self.0.children().count()
1033            }
1034        };
1035
1036        // Build the nodes to insert
1037        let mut to_insert = vec![];
1038
1039        // Determine if we need to add an EMPTY_LINE before the paragraph
1040        let needs_empty_line_before = if insert_at == 0 {
1041            // At the beginning - no empty line before
1042            false
1043        } else if insert_at > 0 {
1044            // Check if there's already an EMPTY_LINE at the insertion point
1045            if let Some(node_at_insert) = self.0.children_with_tokens().nth(insert_at - 1) {
1046                node_at_insert.kind() != EMPTY_LINE
1047            } else {
1048                false
1049            }
1050        } else {
1051            false
1052        };
1053
1054        if needs_empty_line_before {
1055            let mut builder = GreenNodeBuilder::new();
1056            builder.start_node(EMPTY_LINE.into());
1057            builder.token(NEWLINE.into(), "\n");
1058            builder.finish_node();
1059            to_insert.push(SyntaxNode::new_root_mut(builder.finish()).into());
1060        }
1061
1062        to_insert.push(paragraph_to_move.into());
1063
1064        // Determine if we need to add an EMPTY_LINE after the paragraph
1065        let needs_empty_line_after = if insert_at < self.0.children().count() {
1066            // There are nodes after - check if next node is EMPTY_LINE
1067            if let Some(node_after) = self.0.children_with_tokens().nth(insert_at) {
1068                node_after.kind() != EMPTY_LINE
1069            } else {
1070                false
1071            }
1072        } else {
1073            false
1074        };
1075
1076        if needs_empty_line_after {
1077            let mut builder = GreenNodeBuilder::new();
1078            builder.start_node(EMPTY_LINE.into());
1079            builder.token(NEWLINE.into(), "\n");
1080            builder.finish_node();
1081            to_insert.push(SyntaxNode::new_root_mut(builder.finish()).into());
1082        }
1083
1084        // Insert at the new position
1085        self.0.splice_children(insert_at..insert_at, to_insert);
1086    }
1087
1088    /// Add a new empty paragraph to the end of the file.
1089    pub fn add_paragraph(&mut self) -> Paragraph {
1090        self.insert_empty_paragraph(None)
1091    }
1092
1093    /// Swap two paragraphs by their indices.
1094    ///
1095    /// This method swaps the positions of two paragraphs while preserving their
1096    /// content, formatting, whitespace, and comments. The paragraphs at positions
1097    /// `index1` and `index2` will exchange places.
1098    ///
1099    /// # Arguments
1100    ///
1101    /// * `index1` - The index of the first paragraph to swap
1102    /// * `index2` - The index of the second paragraph to swap
1103    ///
1104    /// # Panics
1105    ///
1106    /// Panics if either `index1` or `index2` is out of bounds.
1107    ///
1108    /// # Examples
1109    ///
1110    /// ```
1111    /// use deb822_lossless::Deb822;
1112    /// let mut d: Deb822 = vec![
1113    ///     vec![("Foo", "Bar")].into_iter().collect(),
1114    ///     vec![("A", "B")].into_iter().collect(),
1115    ///     vec![("X", "Y")].into_iter().collect(),
1116    /// ]
1117    /// .into_iter()
1118    /// .collect();
1119    /// d.swap_paragraphs(0, 2);
1120    /// assert_eq!(d.to_string(), "X: Y\n\nA: B\n\nFoo: Bar\n");
1121    /// ```
1122    pub fn swap_paragraphs(&mut self, index1: usize, index2: usize) {
1123        if index1 == index2 {
1124            return;
1125        }
1126
1127        // Collect all children
1128        let mut children: Vec<_> = self.0.children().map(|n| n.clone().into()).collect();
1129
1130        // Find the child indices for paragraphs
1131        let mut para_child_indices = vec![];
1132        for (child_idx, child) in self.0.children().enumerate() {
1133            if child.kind() == PARAGRAPH {
1134                para_child_indices.push(child_idx);
1135            }
1136        }
1137
1138        // Validate paragraph indices
1139        if index1 >= para_child_indices.len() {
1140            panic!("index1 {} out of bounds", index1);
1141        }
1142        if index2 >= para_child_indices.len() {
1143            panic!("index2 {} out of bounds", index2);
1144        }
1145
1146        let child_idx1 = para_child_indices[index1];
1147        let child_idx2 = para_child_indices[index2];
1148
1149        // Swap the children in the vector
1150        children.swap(child_idx1, child_idx2);
1151
1152        // Replace all children
1153        let num_children = children.len();
1154        self.0.splice_children(0..num_children, children);
1155    }
1156
1157    /// Read a deb822 file from the given path.
1158    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
1159        let text = std::fs::read_to_string(path)?;
1160        Ok(Self::from_str(&text)?)
1161    }
1162
1163    /// Read a deb822 file from the given path, ignoring any syntax errors.
1164    pub fn from_file_relaxed(
1165        path: impl AsRef<Path>,
1166    ) -> Result<(Self, Vec<String>), std::io::Error> {
1167        let text = std::fs::read_to_string(path)?;
1168        Ok(Self::from_str_relaxed(&text))
1169    }
1170
1171    /// Parse a deb822 file from a string, allowing syntax errors.
1172    pub fn from_str_relaxed(s: &str) -> (Self, Vec<String>) {
1173        let parsed = parse(s);
1174        (parsed.root_mut(), parsed.errors)
1175    }
1176
1177    /// Read a deb822 file from a Read object.
1178    pub fn read<R: std::io::Read>(mut r: R) -> Result<Self, Error> {
1179        let mut buf = String::new();
1180        r.read_to_string(&mut buf)?;
1181        Ok(Self::from_str(&buf)?)
1182    }
1183
1184    /// Read a deb822 file from a Read object, allowing syntax errors.
1185    pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<(Self, Vec<String>), std::io::Error> {
1186        let mut buf = String::new();
1187        r.read_to_string(&mut buf)?;
1188        Ok(Self::from_str_relaxed(&buf))
1189    }
1190}
1191
1192fn inject(builder: &mut GreenNodeBuilder, node: SyntaxNode) {
1193    builder.start_node(node.kind().into());
1194    for child in node.children_with_tokens() {
1195        match child {
1196            rowan::NodeOrToken::Node(child) => {
1197                inject(builder, child);
1198            }
1199            rowan::NodeOrToken::Token(token) => {
1200                builder.token(token.kind().into(), token.text());
1201            }
1202        }
1203    }
1204    builder.finish_node();
1205}
1206
1207impl FromIterator<Paragraph> for Deb822 {
1208    fn from_iter<T: IntoIterator<Item = Paragraph>>(iter: T) -> Self {
1209        let mut builder = GreenNodeBuilder::new();
1210        builder.start_node(ROOT.into());
1211        for (i, paragraph) in iter.into_iter().enumerate() {
1212            if i > 0 {
1213                builder.start_node(EMPTY_LINE.into());
1214                builder.token(NEWLINE.into(), "\n");
1215                builder.finish_node();
1216            }
1217            inject(&mut builder, paragraph.0);
1218        }
1219        builder.finish_node();
1220        Self(SyntaxNode::new_root_mut(builder.finish()))
1221    }
1222}
1223
1224impl From<Vec<(String, String)>> for Paragraph {
1225    fn from(v: Vec<(String, String)>) -> Self {
1226        v.into_iter().collect()
1227    }
1228}
1229
1230impl From<Vec<(&str, &str)>> for Paragraph {
1231    fn from(v: Vec<(&str, &str)>) -> Self {
1232        v.into_iter().collect()
1233    }
1234}
1235
1236impl FromIterator<(String, String)> for Paragraph {
1237    fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
1238        let mut builder = GreenNodeBuilder::new();
1239        builder.start_node(PARAGRAPH.into());
1240        for (key, value) in iter {
1241            builder.start_node(ENTRY.into());
1242            builder.token(KEY.into(), &key);
1243            builder.token(COLON.into(), ":");
1244            builder.token(WHITESPACE.into(), " ");
1245            for (i, line) in value.split('\n').enumerate() {
1246                if i > 0 {
1247                    builder.token(INDENT.into(), " ");
1248                }
1249                builder.token(VALUE.into(), line);
1250                builder.token(NEWLINE.into(), "\n");
1251            }
1252            builder.finish_node();
1253        }
1254        builder.finish_node();
1255        Self(SyntaxNode::new_root_mut(builder.finish()))
1256    }
1257}
1258
1259impl<'a> FromIterator<(&'a str, &'a str)> for Paragraph {
1260    fn from_iter<T: IntoIterator<Item = (&'a str, &'a str)>>(iter: T) -> Self {
1261        let mut builder = GreenNodeBuilder::new();
1262        builder.start_node(PARAGRAPH.into());
1263        for (key, value) in iter {
1264            builder.start_node(ENTRY.into());
1265            builder.token(KEY.into(), key);
1266            builder.token(COLON.into(), ":");
1267            builder.token(WHITESPACE.into(), " ");
1268            for (i, line) in value.split('\n').enumerate() {
1269                if i > 0 {
1270                    builder.token(INDENT.into(), " ");
1271                }
1272                builder.token(VALUE.into(), line);
1273                builder.token(NEWLINE.into(), "\n");
1274            }
1275            builder.finish_node();
1276        }
1277        builder.finish_node();
1278        Self(SyntaxNode::new_root_mut(builder.finish()))
1279    }
1280}
1281
1282/// Detected indentation pattern for multi-line field values
1283#[derive(Debug, Clone, PartialEq, Eq)]
1284pub enum IndentPattern {
1285    /// All fields use a fixed number of spaces for indentation
1286    Fixed(usize),
1287    /// Each field's indentation matches its field name length + 2 (for ": ")
1288    FieldNameLength,
1289}
1290
1291impl IndentPattern {
1292    /// Convert the pattern to a concrete indentation string for a given field name
1293    fn to_string(&self, field_name: &str) -> String {
1294        match self {
1295            IndentPattern::Fixed(spaces) => " ".repeat(*spaces),
1296            IndentPattern::FieldNameLength => " ".repeat(field_name.len() + 2),
1297        }
1298    }
1299}
1300
1301impl Paragraph {
1302    /// Create a new empty paragraph.
1303    pub fn new() -> Paragraph {
1304        let mut builder = GreenNodeBuilder::new();
1305
1306        builder.start_node(PARAGRAPH.into());
1307        builder.finish_node();
1308        Paragraph(SyntaxNode::new_root_mut(builder.finish()))
1309    }
1310
1311    /// Create an independent snapshot of this Paragraph.
1312    ///
1313    /// This creates a new mutable tree that shares the same underlying immutable
1314    /// GreenNode data. Modifications to the original will not affect the snapshot
1315    /// and vice versa.
1316    ///
1317    /// This is more efficient than serializing and re-parsing because it reuses
1318    /// the GreenNode structure.
1319    pub fn snapshot(&self) -> Self {
1320        Paragraph(SyntaxNode::new_root_mut(self.0.green().into_owned()))
1321    }
1322
1323    /// Reformat this paragraph
1324    ///
1325    /// # Arguments
1326    /// * `indentation` - The indentation to use
1327    /// * `immediate_empty_line` - Whether multi-line values should always start with an empty line
1328    /// * `max_line_length_one_liner` - If set, then this is the max length of the value if it is
1329    ///   crammed into a "one-liner" value
1330    /// * `sort_entries` - If set, then this function will sort the entries according to the given
1331    ///   function
1332    /// * `format_value` - If set, then this function will format the value according to the given
1333    ///   function
1334    #[must_use]
1335    pub fn wrap_and_sort(
1336        &self,
1337        indentation: Indentation,
1338        immediate_empty_line: bool,
1339        max_line_length_one_liner: Option<usize>,
1340        sort_entries: Option<&dyn Fn(&Entry, &Entry) -> std::cmp::Ordering>,
1341        format_value: Option<&dyn Fn(&str, &str) -> String>,
1342    ) -> Paragraph {
1343        let mut builder = GreenNodeBuilder::new();
1344
1345        let mut current = vec![];
1346        let mut entries = vec![];
1347
1348        builder.start_node(PARAGRAPH.into());
1349        for c in self.0.children_with_tokens() {
1350            match c.kind() {
1351                ENTRY => {
1352                    entries.push((current, Entry::cast(c.as_node().unwrap().clone()).unwrap()));
1353                    current = vec![];
1354                }
1355                ERROR | COMMENT => {
1356                    current.push(c);
1357                }
1358                _ => {}
1359            }
1360        }
1361
1362        if let Some(sort_entry) = sort_entries {
1363            entries.sort_by(|a, b| {
1364                let a_key = &a.1;
1365                let b_key = &b.1;
1366                sort_entry(a_key, b_key)
1367            });
1368        }
1369
1370        for (pre, entry) in entries.into_iter() {
1371            for c in pre.into_iter() {
1372                builder.token(c.kind().into(), c.as_token().unwrap().text());
1373            }
1374
1375            inject(
1376                &mut builder,
1377                entry
1378                    .wrap_and_sort(
1379                        indentation,
1380                        immediate_empty_line,
1381                        max_line_length_one_liner,
1382                        format_value,
1383                    )
1384                    .0,
1385            );
1386        }
1387
1388        for c in current {
1389            builder.token(c.kind().into(), c.as_token().unwrap().text());
1390        }
1391
1392        builder.finish_node();
1393        Self(SyntaxNode::new_root_mut(builder.finish()))
1394    }
1395
1396    /// Normalize the spacing around field separators (colons) for all entries in place.
1397    ///
1398    /// This ensures that there is exactly one space after the colon and before the value
1399    /// for each field in the paragraph. This is a lossless operation that preserves the
1400    /// field names, values, and comments, but normalizes the whitespace formatting.
1401    ///
1402    /// # Examples
1403    ///
1404    /// ```
1405    /// use deb822_lossless::Deb822;
1406    /// use std::str::FromStr;
1407    ///
1408    /// let input = "Field1:    value1\nField2:value2\n";
1409    /// let mut deb822 = Deb822::from_str(input).unwrap();
1410    /// let mut para = deb822.paragraphs().next().unwrap();
1411    ///
1412    /// para.normalize_field_spacing();
1413    /// assert_eq!(para.to_string(), "Field1: value1\nField2: value2\n");
1414    /// ```
1415    pub fn normalize_field_spacing(&mut self) -> bool {
1416        let mut any_changed = false;
1417
1418        // Collect entries to avoid borrowing issues
1419        let mut entries: Vec<_> = self.entries().collect();
1420
1421        // Normalize each entry
1422        for entry in &mut entries {
1423            if entry.normalize_field_spacing() {
1424                any_changed = true;
1425            }
1426        }
1427
1428        any_changed
1429    }
1430
1431    /// Returns the value of the given key in the paragraph.
1432    ///
1433    /// Field names are compared case-insensitively.
1434    pub fn get(&self, key: &str) -> Option<String> {
1435        self.entries()
1436            .find(|e| {
1437                e.key()
1438                    .as_deref()
1439                    .is_some_and(|k| k.eq_ignore_ascii_case(key))
1440            })
1441            .map(|e| e.value())
1442    }
1443
1444    /// Returns the entry for the given key in the paragraph.
1445    ///
1446    /// Field names are compared case-insensitively.
1447    pub fn get_entry(&self, key: &str) -> Option<Entry> {
1448        self.entries().find(|e| {
1449            e.key()
1450                .as_deref()
1451                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1452        })
1453    }
1454
1455    /// Returns the value of the given key with a specific indentation pattern applied.
1456    ///
1457    /// This returns the field value reformatted as if it were written with the specified
1458    /// indentation pattern. For single-line values, this is the same as `get()`.
1459    /// For multi-line values, the continuation lines are prefixed with indentation
1460    /// calculated from the indent pattern.
1461    ///
1462    /// Field names are compared case-insensitively.
1463    ///
1464    /// # Arguments
1465    /// * `key` - The field name to retrieve
1466    /// * `indent_pattern` - The indentation pattern to apply
1467    ///
1468    /// # Example
1469    /// ```
1470    /// use deb822_lossless::{Deb822, IndentPattern};
1471    /// use std::str::FromStr;
1472    ///
1473    /// let input = "Field: First\n   Second\n   Third\n";
1474    /// let deb = Deb822::from_str(input).unwrap();
1475    /// let para = deb.paragraphs().next().unwrap();
1476    ///
1477    /// // Get with fixed 2-space indentation - strips 2 spaces from each line
1478    /// let value = para.get_with_indent("Field", &IndentPattern::Fixed(2)).unwrap();
1479    /// assert_eq!(value, "First\n Second\n Third");
1480    /// ```
1481    pub fn get_with_indent(&self, key: &str, indent_pattern: &IndentPattern) -> Option<String> {
1482        use crate::lex::SyntaxKind::{INDENT, VALUE};
1483
1484        self.entries()
1485            .find(|e| {
1486                e.key()
1487                    .as_deref()
1488                    .is_some_and(|k| k.eq_ignore_ascii_case(key))
1489            })
1490            .and_then(|e| {
1491                let field_key = e.key()?;
1492                let expected_indent = indent_pattern.to_string(&field_key);
1493                let expected_len = expected_indent.len();
1494
1495                let mut result = String::new();
1496                let mut first = true;
1497                let mut last_indent: Option<String> = None;
1498
1499                for token in e.0.children_with_tokens().filter_map(|it| it.into_token()) {
1500                    match token.kind() {
1501                        INDENT => {
1502                            last_indent = Some(token.text().to_string());
1503                        }
1504                        VALUE => {
1505                            if !first {
1506                                result.push('\n');
1507                                // Add any indentation beyond the expected amount
1508                                if let Some(ref indent_text) = last_indent {
1509                                    if indent_text.len() > expected_len {
1510                                        result.push_str(&indent_text[expected_len..]);
1511                                    }
1512                                }
1513                            }
1514                            result.push_str(token.text());
1515                            first = false;
1516                            last_indent = None;
1517                        }
1518                        _ => {}
1519                    }
1520                }
1521
1522                Some(result)
1523            })
1524    }
1525
1526    /// Get a multi-line field value with single-space indentation stripped.
1527    ///
1528    /// This is a convenience wrapper around `get_with_indent()` that uses
1529    /// `IndentPattern::Fixed(1)`, which is the standard indentation for
1530    /// multi-line fields in Debian control files.
1531    ///
1532    /// # Arguments
1533    ///
1534    /// * `key` - The field name (case-insensitive)
1535    ///
1536    /// # Returns
1537    ///
1538    /// The field value with single-space indentation stripped from continuation lines,
1539    /// or `None` if the field doesn't exist.
1540    ///
1541    /// # Example
1542    ///
1543    /// ```
1544    /// use deb822_lossless::Deb822;
1545    ///
1546    /// let text = "Description: Short description\n Additional line\n";
1547    /// let deb822 = Deb822::parse(text).tree();
1548    /// let para = deb822.paragraphs().next().unwrap();
1549    /// let value = para.get_multiline("Description").unwrap();
1550    /// assert_eq!(value, "Short description\nAdditional line");
1551    /// ```
1552    pub fn get_multiline(&self, key: &str) -> Option<String> {
1553        self.get_with_indent(key, &IndentPattern::Fixed(1))
1554    }
1555
1556    /// Set a multi-line field value with single-space indentation.
1557    ///
1558    /// This is a convenience wrapper around `try_set_with_forced_indent()` that uses
1559    /// `IndentPattern::Fixed(1)`, which is the standard indentation for
1560    /// multi-line fields in Debian control files.
1561    ///
1562    /// # Arguments
1563    ///
1564    /// * `key` - The field name
1565    /// * `value` - The field value (will be formatted with single-space indentation)
1566    /// * `field_order` - Optional field ordering specification
1567    ///
1568    /// # Returns
1569    ///
1570    /// `Ok(())` if successful, or an `Error` if the value is invalid.
1571    ///
1572    /// # Example
1573    ///
1574    /// ```
1575    /// use deb822_lossless::Paragraph;
1576    ///
1577    /// let mut para = Paragraph::new();
1578    /// para.set_multiline("Description", "Short description\nAdditional line", None).unwrap();
1579    /// assert_eq!(para.get_multiline("Description").unwrap(), "Short description\nAdditional line");
1580    /// ```
1581    pub fn set_multiline(
1582        &mut self,
1583        key: &str,
1584        value: &str,
1585        field_order: Option<&[&str]>,
1586    ) -> Result<(), Error> {
1587        self.try_set_with_forced_indent(key, value, &IndentPattern::Fixed(1), field_order)
1588    }
1589
1590    /// Returns whether the paragraph contains the given key.
1591    pub fn contains_key(&self, key: &str) -> bool {
1592        self.get(key).is_some()
1593    }
1594
1595    /// Returns an iterator over all entries in the paragraph.
1596    pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
1597        self.0.children().filter_map(Entry::cast)
1598    }
1599
1600    /// Returns an iterator over all items in the paragraph.
1601    pub fn items(&self) -> impl Iterator<Item = (String, String)> + '_ {
1602        self.entries()
1603            .filter_map(|e| e.key().map(|k| (k, e.value())))
1604    }
1605
1606    /// Returns an iterator over all values for the given key in the paragraph.
1607    ///
1608    /// Field names are compared case-insensitively.
1609    pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = String> + 'a {
1610        self.items().filter_map(move |(k, v)| {
1611            if k.eq_ignore_ascii_case(key) {
1612                Some(v)
1613            } else {
1614                None
1615            }
1616        })
1617    }
1618
1619    /// Returns an iterator over all keys in the paragraph.
1620    pub fn keys(&self) -> impl Iterator<Item = String> + '_ {
1621        self.entries().filter_map(|e| e.key())
1622    }
1623
1624    /// Remove the given field from the paragraph.
1625    ///
1626    /// Field names are compared case-insensitively.
1627    pub fn remove(&mut self, key: &str) {
1628        for mut entry in self.entries() {
1629            if entry
1630                .key()
1631                .as_deref()
1632                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1633            {
1634                entry.detach();
1635            }
1636        }
1637    }
1638
1639    /// Insert a new field
1640    pub fn insert(&mut self, key: &str, value: &str) {
1641        let entry = Entry::new(key, value);
1642        let count = self.0.children_with_tokens().count();
1643        self.0.splice_children(count..count, vec![entry.0.into()]);
1644    }
1645
1646    /// Insert a comment line before this paragraph.
1647    ///
1648    /// The comment should not include the leading '#' character or newline,
1649    /// these will be added automatically.
1650    ///
1651    /// # Examples
1652    ///
1653    /// ```
1654    /// use deb822_lossless::Deb822;
1655    /// let mut d: Deb822 = vec![
1656    ///     vec![("Foo", "Bar")].into_iter().collect(),
1657    /// ]
1658    /// .into_iter()
1659    /// .collect();
1660    /// let mut para = d.paragraphs().next().unwrap();
1661    /// para.insert_comment_before("This is a comment");
1662    /// assert_eq!(d.to_string(), "# This is a comment\nFoo: Bar\n");
1663    /// ```
1664    pub fn insert_comment_before(&mut self, comment: &str) {
1665        use rowan::GreenNodeBuilder;
1666
1667        // Create an EMPTY_LINE node containing the comment tokens
1668        // This matches the structure used elsewhere in the parser
1669        let mut builder = GreenNodeBuilder::new();
1670        builder.start_node(EMPTY_LINE.into());
1671        builder.token(COMMENT.into(), &format!("# {}", comment));
1672        builder.token(NEWLINE.into(), "\n");
1673        builder.finish_node();
1674        let green = builder.finish();
1675
1676        // Convert to syntax node and insert before this paragraph
1677        let comment_node = SyntaxNode::new_root_mut(green);
1678
1679        let index = self.0.index();
1680        let parent = self.0.parent().expect("Paragraph must have a parent");
1681        parent.splice_children(index..index, vec![comment_node.into()]);
1682    }
1683
1684    /// Detect the indentation pattern used in this paragraph.
1685    ///
1686    /// This method analyzes existing multi-line fields to determine if they use:
1687    /// 1. A fixed indentation (all fields use the same number of spaces)
1688    /// 2. Field-name-length-based indentation (indent matches field name + ": ")
1689    ///
1690    /// If no pattern can be detected, defaults to field name length + 2.
1691    fn detect_indent_pattern(&self) -> IndentPattern {
1692        // Collect indentation data from existing multi-line fields
1693        let indent_data: Vec<(String, usize)> = self
1694            .entries()
1695            .filter_map(|entry| {
1696                let field_key = entry.key()?;
1697                let indent = entry.get_indent()?;
1698                Some((field_key, indent.len()))
1699            })
1700            .collect();
1701
1702        if indent_data.is_empty() {
1703            // No existing multi-line fields, default to field name length
1704            return IndentPattern::FieldNameLength;
1705        }
1706
1707        // Check if all fields use the same fixed indentation
1708        let first_indent_len = indent_data[0].1;
1709        let all_same = indent_data.iter().all(|(_, len)| *len == first_indent_len);
1710
1711        if all_same {
1712            // All fields use the same indentation - use that
1713            return IndentPattern::Fixed(first_indent_len);
1714        }
1715
1716        // Check if fields use field-name-length-based indentation
1717        let all_match_field_length = indent_data
1718            .iter()
1719            .all(|(field_key, indent_len)| *indent_len == field_key.len() + 2);
1720
1721        if all_match_field_length {
1722            // Fields use field-name-length-based indentation
1723            return IndentPattern::FieldNameLength;
1724        }
1725
1726        // Can't detect a clear pattern, default to field name length + 2
1727        IndentPattern::FieldNameLength
1728    }
1729
1730    /// Try to set a field in the paragraph, inserting at the appropriate location if new.
1731    ///
1732    /// # Errors
1733    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
1734    pub fn try_set(&mut self, key: &str, value: &str) -> Result<(), Error> {
1735        self.try_set_with_indent_pattern(key, value, None, None)
1736    }
1737
1738    /// Set a field in the paragraph, inserting at the appropriate location if new
1739    ///
1740    /// # Panics
1741    /// Panics if the value contains empty continuation lines (lines with only whitespace)
1742    pub fn set(&mut self, key: &str, value: &str) {
1743        self.try_set(key, value)
1744            .expect("Invalid value: empty continuation line")
1745    }
1746
1747    /// Set a field using a specific field ordering
1748    pub fn set_with_field_order(&mut self, key: &str, value: &str, field_order: &[&str]) {
1749        self.try_set_with_indent_pattern(key, value, None, Some(field_order))
1750            .expect("Invalid value: empty continuation line")
1751    }
1752
1753    /// Try to set a field with optional default indentation pattern and field ordering.
1754    ///
1755    /// This method allows setting a field while optionally specifying a default indentation pattern
1756    /// to use when the field doesn't already have multi-line indentation to preserve.
1757    /// If the field already exists and is multi-line, its existing indentation is preserved.
1758    ///
1759    /// # Arguments
1760    /// * `key` - The field name
1761    /// * `value` - The field value
1762    /// * `default_indent_pattern` - Optional default indentation pattern to use for new fields or
1763    ///   fields without existing multi-line indentation. If None, will preserve existing field's
1764    ///   indentation or auto-detect from other fields
1765    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
1766    ///
1767    /// # Errors
1768    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
1769    pub fn try_set_with_indent_pattern(
1770        &mut self,
1771        key: &str,
1772        value: &str,
1773        default_indent_pattern: Option<&IndentPattern>,
1774        field_order: Option<&[&str]>,
1775    ) -> Result<(), Error> {
1776        // Check if the field already exists and extract its formatting (case-insensitive)
1777        let existing_entry = self.entries().find(|entry| {
1778            entry
1779                .key()
1780                .as_deref()
1781                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1782        });
1783
1784        // Determine indentation to use
1785        let indent = existing_entry
1786            .as_ref()
1787            .and_then(|entry| entry.get_indent())
1788            .unwrap_or_else(|| {
1789                // No existing indentation, use default pattern or auto-detect
1790                if let Some(pattern) = default_indent_pattern {
1791                    pattern.to_string(key)
1792                } else {
1793                    self.detect_indent_pattern().to_string(key)
1794                }
1795            });
1796
1797        let post_colon_ws = existing_entry
1798            .as_ref()
1799            .and_then(|entry| entry.get_post_colon_whitespace())
1800            .unwrap_or_else(|| " ".to_string());
1801
1802        // When replacing an existing field, preserve the original case of the field name
1803        let actual_key = existing_entry
1804            .as_ref()
1805            .and_then(|e| e.key())
1806            .unwrap_or_else(|| key.to_string());
1807
1808        let new_entry = Entry::try_with_formatting(&actual_key, value, &post_colon_ws, &indent)?;
1809
1810        // Check if the field already exists and replace it (case-insensitive)
1811        for entry in self.entries() {
1812            if entry
1813                .key()
1814                .as_deref()
1815                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1816            {
1817                self.0.splice_children(
1818                    entry.0.index()..entry.0.index() + 1,
1819                    vec![new_entry.0.into()],
1820                );
1821                return Ok(());
1822            }
1823        }
1824
1825        // Insert new field
1826        if let Some(order) = field_order {
1827            let insertion_index = self.find_insertion_index(key, order);
1828            self.0
1829                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
1830        } else {
1831            // Insert at the end if no field order specified
1832            let insertion_index = self.0.children_with_tokens().count();
1833            self.0
1834                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
1835        }
1836        Ok(())
1837    }
1838
1839    /// Set a field with optional default indentation pattern and field ordering.
1840    ///
1841    /// This method allows setting a field while optionally specifying a default indentation pattern
1842    /// to use when the field doesn't already have multi-line indentation to preserve.
1843    /// If the field already exists and is multi-line, its existing indentation is preserved.
1844    ///
1845    /// # Arguments
1846    /// * `key` - The field name
1847    /// * `value` - The field value
1848    /// * `default_indent_pattern` - Optional default indentation pattern to use for new fields or
1849    ///   fields without existing multi-line indentation. If None, will preserve existing field's
1850    ///   indentation or auto-detect from other fields
1851    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
1852    ///
1853    /// # Panics
1854    /// Panics if the value contains empty continuation lines (lines with only whitespace)
1855    pub fn set_with_indent_pattern(
1856        &mut self,
1857        key: &str,
1858        value: &str,
1859        default_indent_pattern: Option<&IndentPattern>,
1860        field_order: Option<&[&str]>,
1861    ) {
1862        self.try_set_with_indent_pattern(key, value, default_indent_pattern, field_order)
1863            .expect("Invalid value: empty continuation line")
1864    }
1865
1866    /// Try to set a field, forcing a specific indentation pattern regardless of existing indentation.
1867    ///
1868    /// Unlike `try_set_with_indent_pattern`, this method does NOT preserve existing field indentation.
1869    /// It always applies the specified indentation pattern to the field.
1870    ///
1871    /// # Arguments
1872    /// * `key` - The field name
1873    /// * `value` - The field value
1874    /// * `indent_pattern` - The indentation pattern to use for this field
1875    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
1876    ///
1877    /// # Errors
1878    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
1879    pub fn try_set_with_forced_indent(
1880        &mut self,
1881        key: &str,
1882        value: &str,
1883        indent_pattern: &IndentPattern,
1884        field_order: Option<&[&str]>,
1885    ) -> Result<(), Error> {
1886        // Check if the field already exists (case-insensitive)
1887        let existing_entry = self.entries().find(|entry| {
1888            entry
1889                .key()
1890                .as_deref()
1891                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1892        });
1893
1894        // Get post-colon whitespace from existing field, or default to single space
1895        let post_colon_ws = existing_entry
1896            .as_ref()
1897            .and_then(|entry| entry.get_post_colon_whitespace())
1898            .unwrap_or_else(|| " ".to_string());
1899
1900        // When replacing an existing field, preserve the original case of the field name
1901        let actual_key = existing_entry
1902            .as_ref()
1903            .and_then(|e| e.key())
1904            .unwrap_or_else(|| key.to_string());
1905
1906        // Force the indentation pattern
1907        let indent = indent_pattern.to_string(&actual_key);
1908        let new_entry = Entry::try_with_formatting(&actual_key, value, &post_colon_ws, &indent)?;
1909
1910        // Check if the field already exists and replace it (case-insensitive)
1911        for entry in self.entries() {
1912            if entry
1913                .key()
1914                .as_deref()
1915                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1916            {
1917                self.0.splice_children(
1918                    entry.0.index()..entry.0.index() + 1,
1919                    vec![new_entry.0.into()],
1920                );
1921                return Ok(());
1922            }
1923        }
1924
1925        // Insert new field
1926        if let Some(order) = field_order {
1927            let insertion_index = self.find_insertion_index(key, order);
1928            self.0
1929                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
1930        } else {
1931            // Insert at the end if no field order specified
1932            let insertion_index = self.0.children_with_tokens().count();
1933            self.0
1934                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
1935        }
1936        Ok(())
1937    }
1938
1939    /// Set a field, forcing a specific indentation pattern regardless of existing indentation.
1940    ///
1941    /// Unlike `set_with_indent_pattern`, this method does NOT preserve existing field indentation.
1942    /// It always applies the specified indentation pattern to the field.
1943    ///
1944    /// # Arguments
1945    /// * `key` - The field name
1946    /// * `value` - The field value
1947    /// * `indent_pattern` - The indentation pattern to use for this field
1948    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
1949    ///
1950    /// # Panics
1951    /// Panics if the value contains empty continuation lines (lines with only whitespace)
1952    pub fn set_with_forced_indent(
1953        &mut self,
1954        key: &str,
1955        value: &str,
1956        indent_pattern: &IndentPattern,
1957        field_order: Option<&[&str]>,
1958    ) {
1959        self.try_set_with_forced_indent(key, value, indent_pattern, field_order)
1960            .expect("Invalid value: empty continuation line")
1961    }
1962
1963    /// Change the indentation of an existing field without modifying its value.
1964    ///
1965    /// This method finds an existing field and reapplies it with a new indentation pattern,
1966    /// preserving the field's current value.
1967    ///
1968    /// # Arguments
1969    /// * `key` - The field name to update
1970    /// * `indent_pattern` - The new indentation pattern to apply
1971    ///
1972    /// # Returns
1973    /// Returns `Ok(true)` if the field was found and updated, `Ok(false)` if the field doesn't exist,
1974    /// or `Err` if there was an error (e.g., invalid value with empty continuation lines)
1975    ///
1976    /// # Errors
1977    /// Returns an error if the field value contains empty continuation lines (lines with only whitespace)
1978    pub fn change_field_indent(
1979        &mut self,
1980        key: &str,
1981        indent_pattern: &IndentPattern,
1982    ) -> Result<bool, Error> {
1983        // Check if the field exists (case-insensitive)
1984        let existing_entry = self.entries().find(|entry| {
1985            entry
1986                .key()
1987                .as_deref()
1988                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1989        });
1990
1991        if let Some(entry) = existing_entry {
1992            let value = entry.value();
1993            let actual_key = entry.key().unwrap_or_else(|| key.to_string());
1994
1995            // Get post-colon whitespace from existing field
1996            let post_colon_ws = entry
1997                .get_post_colon_whitespace()
1998                .unwrap_or_else(|| " ".to_string());
1999
2000            // Apply the new indentation pattern
2001            let indent = indent_pattern.to_string(&actual_key);
2002            let new_entry =
2003                Entry::try_with_formatting(&actual_key, &value, &post_colon_ws, &indent)?;
2004
2005            // Replace the existing entry
2006            self.0.splice_children(
2007                entry.0.index()..entry.0.index() + 1,
2008                vec![new_entry.0.into()],
2009            );
2010            Ok(true)
2011        } else {
2012            Ok(false)
2013        }
2014    }
2015
2016    /// Find the appropriate insertion index for a new field based on field ordering
2017    fn find_insertion_index(&self, key: &str, field_order: &[&str]) -> usize {
2018        // Find position of the new field in the canonical order (case-insensitive)
2019        let new_field_position = field_order
2020            .iter()
2021            .position(|&field| field.eq_ignore_ascii_case(key));
2022
2023        let mut insertion_index = self.0.children_with_tokens().count();
2024
2025        // Find the right position based on canonical field order
2026        for (i, child) in self.0.children_with_tokens().enumerate() {
2027            if let Some(node) = child.as_node() {
2028                if let Some(entry) = Entry::cast(node.clone()) {
2029                    if let Some(existing_key) = entry.key() {
2030                        let existing_position = field_order
2031                            .iter()
2032                            .position(|&field| field.eq_ignore_ascii_case(&existing_key));
2033
2034                        match (new_field_position, existing_position) {
2035                            // Both fields are in the canonical order
2036                            (Some(new_pos), Some(existing_pos)) => {
2037                                if new_pos < existing_pos {
2038                                    insertion_index = i;
2039                                    break;
2040                                }
2041                            }
2042                            // New field is in canonical order, existing is not
2043                            (Some(_), None) => {
2044                                // Continue looking - unknown fields go after known ones
2045                            }
2046                            // New field is not in canonical order, existing is
2047                            (None, Some(_)) => {
2048                                // Continue until we find all known fields
2049                            }
2050                            // Neither field is in canonical order, maintain alphabetical
2051                            (None, None) => {
2052                                if key < existing_key.as_str() {
2053                                    insertion_index = i;
2054                                    break;
2055                                }
2056                            }
2057                        }
2058                    }
2059                }
2060            }
2061        }
2062
2063        // If we have a position in canonical order but haven't found where to insert yet,
2064        // we need to insert after all known fields that come before it
2065        if new_field_position.is_some() && insertion_index == self.0.children_with_tokens().count()
2066        {
2067            // Look for the position after the last known field that comes before our field
2068            let children: Vec<_> = self.0.children_with_tokens().enumerate().collect();
2069            for (i, child) in children.into_iter().rev() {
2070                if let Some(node) = child.as_node() {
2071                    if let Some(entry) = Entry::cast(node.clone()) {
2072                        if let Some(existing_key) = entry.key() {
2073                            if field_order
2074                                .iter()
2075                                .any(|&f| f.eq_ignore_ascii_case(&existing_key))
2076                            {
2077                                // Found a known field, insert after it
2078                                insertion_index = i + 1;
2079                                break;
2080                            }
2081                        }
2082                    }
2083                }
2084            }
2085        }
2086
2087        insertion_index
2088    }
2089
2090    /// Rename the given field in the paragraph.
2091    ///
2092    /// Field names are compared case-insensitively.
2093    pub fn rename(&mut self, old_key: &str, new_key: &str) -> bool {
2094        for entry in self.entries() {
2095            if entry
2096                .key()
2097                .as_deref()
2098                .is_some_and(|k| k.eq_ignore_ascii_case(old_key))
2099            {
2100                self.0.splice_children(
2101                    entry.0.index()..entry.0.index() + 1,
2102                    vec![Entry::new(new_key, entry.value().as_str()).0.into()],
2103                );
2104                return true;
2105            }
2106        }
2107        false
2108    }
2109}
2110
2111impl Default for Paragraph {
2112    fn default() -> Self {
2113        Self::new()
2114    }
2115}
2116
2117impl std::str::FromStr for Paragraph {
2118    type Err = ParseError;
2119
2120    fn from_str(text: &str) -> Result<Self, Self::Err> {
2121        let deb822 = Deb822::from_str(text)?;
2122
2123        let mut paragraphs = deb822.paragraphs();
2124
2125        paragraphs
2126            .next()
2127            .ok_or_else(|| ParseError(vec!["no paragraphs".to_string()]))
2128    }
2129}
2130
2131#[cfg(feature = "python-debian")]
2132impl<'py> pyo3::IntoPyObject<'py> for Paragraph {
2133    type Target = pyo3::PyAny;
2134    type Output = pyo3::Bound<'py, Self::Target>;
2135    type Error = pyo3::PyErr;
2136
2137    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
2138        use pyo3::prelude::*;
2139        let d = pyo3::types::PyDict::new(py);
2140        for (k, v) in self.items() {
2141            d.set_item(k, v)?;
2142        }
2143        let m = py.import("debian.deb822")?;
2144        let cls = m.getattr("Deb822")?;
2145        cls.call1((d,))
2146    }
2147}
2148
2149#[cfg(feature = "python-debian")]
2150impl<'py> pyo3::IntoPyObject<'py> for &Paragraph {
2151    type Target = pyo3::PyAny;
2152    type Output = pyo3::Bound<'py, Self::Target>;
2153    type Error = pyo3::PyErr;
2154
2155    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
2156        use pyo3::prelude::*;
2157        let d = pyo3::types::PyDict::new(py);
2158        for (k, v) in self.items() {
2159            d.set_item(k, v)?;
2160        }
2161        let m = py.import("debian.deb822")?;
2162        let cls = m.getattr("Deb822")?;
2163        cls.call1((d,))
2164    }
2165}
2166
2167#[cfg(feature = "python-debian")]
2168impl<'py> pyo3::FromPyObject<'_, 'py> for Paragraph {
2169    type Error = pyo3::PyErr;
2170
2171    fn extract(obj: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
2172        use pyo3::types::PyAnyMethods;
2173        let d = obj.call_method0("__str__")?.extract::<String>()?;
2174        Paragraph::from_str(&d)
2175            .map_err(|e| pyo3::exceptions::PyValueError::new_err((e.to_string(),)))
2176    }
2177}
2178
2179impl Entry {
2180    /// Create an independent snapshot of this Entry.
2181    ///
2182    /// This creates a new mutable tree that shares the same underlying immutable
2183    /// GreenNode data. Modifications to the original will not affect the snapshot
2184    /// and vice versa.
2185    ///
2186    /// This is more efficient than serializing and re-parsing because it reuses
2187    /// the GreenNode structure.
2188    pub fn snapshot(&self) -> Self {
2189        Entry(SyntaxNode::new_root_mut(self.0.green().into_owned()))
2190    }
2191
2192    /// Returns the text range of this entry in the source text.
2193    pub fn text_range(&self) -> rowan::TextRange {
2194        self.0.text_range()
2195    }
2196
2197    /// Returns the text range of the key (field name) in this entry.
2198    pub fn key_range(&self) -> Option<rowan::TextRange> {
2199        self.0
2200            .children_with_tokens()
2201            .filter_map(|it| it.into_token())
2202            .find(|it| it.kind() == KEY)
2203            .map(|it| it.text_range())
2204    }
2205
2206    /// Returns the text range of the colon separator in this entry.
2207    pub fn colon_range(&self) -> Option<rowan::TextRange> {
2208        self.0
2209            .children_with_tokens()
2210            .filter_map(|it| it.into_token())
2211            .find(|it| it.kind() == COLON)
2212            .map(|it| it.text_range())
2213    }
2214
2215    /// Returns the text range of the value portion (excluding the key and colon) in this entry.
2216    /// This includes all VALUE tokens and any continuation lines.
2217    pub fn value_range(&self) -> Option<rowan::TextRange> {
2218        let value_tokens: Vec<_> = self
2219            .0
2220            .children_with_tokens()
2221            .filter_map(|it| it.into_token())
2222            .filter(|it| it.kind() == VALUE)
2223            .collect();
2224
2225        if value_tokens.is_empty() {
2226            return None;
2227        }
2228
2229        let first = value_tokens.first().unwrap();
2230        let last = value_tokens.last().unwrap();
2231        Some(rowan::TextRange::new(
2232            first.text_range().start(),
2233            last.text_range().end(),
2234        ))
2235    }
2236
2237    /// Returns the text ranges of all individual value lines in this entry.
2238    /// Multi-line values will return multiple ranges.
2239    pub fn value_line_ranges(&self) -> Vec<rowan::TextRange> {
2240        self.0
2241            .children_with_tokens()
2242            .filter_map(|it| it.into_token())
2243            .filter(|it| it.kind() == VALUE)
2244            .map(|it| it.text_range())
2245            .collect()
2246    }
2247
2248    /// Create a new entry with the given key and value.
2249    pub fn new(key: &str, value: &str) -> Entry {
2250        Self::with_indentation(key, value, " ")
2251    }
2252
2253    /// Create a new entry with the given key, value, and custom indentation for continuation lines.
2254    ///
2255    /// # Arguments
2256    /// * `key` - The field name
2257    /// * `value` - The field value (may contain '\n' for multi-line values)
2258    /// * `indent` - The indentation string to use for continuation lines
2259    pub fn with_indentation(key: &str, value: &str, indent: &str) -> Entry {
2260        Entry::with_formatting(key, value, " ", indent)
2261    }
2262
2263    /// Try to create a new entry with specific formatting, validating the value.
2264    ///
2265    /// # Arguments
2266    /// * `key` - The field name
2267    /// * `value` - The field value (may contain '\n' for multi-line values)
2268    /// * `post_colon_ws` - The whitespace after the colon (e.g., " " or "\n ")
2269    /// * `indent` - The indentation string to use for continuation lines
2270    ///
2271    /// # Errors
2272    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
2273    pub fn try_with_formatting(
2274        key: &str,
2275        value: &str,
2276        post_colon_ws: &str,
2277        indent: &str,
2278    ) -> Result<Entry, Error> {
2279        let mut builder = GreenNodeBuilder::new();
2280
2281        builder.start_node(ENTRY.into());
2282        builder.token(KEY.into(), key);
2283        builder.token(COLON.into(), ":");
2284
2285        // Add the post-colon whitespace token by token
2286        let mut i = 0;
2287        while i < post_colon_ws.len() {
2288            if post_colon_ws[i..].starts_with('\n') {
2289                builder.token(NEWLINE.into(), "\n");
2290                i += 1;
2291            } else {
2292                // Collect consecutive non-newline chars as WHITESPACE
2293                let start = i;
2294                while i < post_colon_ws.len() && !post_colon_ws[i..].starts_with('\n') {
2295                    i += post_colon_ws[i..].chars().next().unwrap().len_utf8();
2296                }
2297                builder.token(WHITESPACE.into(), &post_colon_ws[start..i]);
2298            }
2299        }
2300
2301        for (line_idx, line) in value.split('\n').enumerate() {
2302            if line_idx > 0 {
2303                // Validate that continuation lines are not empty or whitespace-only
2304                // According to Debian Policy, continuation lines must have content
2305                if line.trim().is_empty() {
2306                    return Err(Error::InvalidValue(format!(
2307                        "empty continuation line (line with only whitespace) at line {}",
2308                        line_idx + 1
2309                    )));
2310                }
2311                builder.token(INDENT.into(), indent);
2312            }
2313            builder.token(VALUE.into(), line);
2314            builder.token(NEWLINE.into(), "\n");
2315        }
2316        builder.finish_node();
2317        Ok(Entry(SyntaxNode::new_root_mut(builder.finish())))
2318    }
2319
2320    /// Create a new entry with specific formatting for post-colon whitespace and indentation.
2321    ///
2322    /// # Arguments
2323    /// * `key` - The field name
2324    /// * `value` - The field value (may contain '\n' for multi-line values)
2325    /// * `post_colon_ws` - The whitespace after the colon (e.g., " " or "\n ")
2326    /// * `indent` - The indentation string to use for continuation lines
2327    ///
2328    /// # Panics
2329    /// Panics if the value contains empty continuation lines (lines with only whitespace)
2330    pub fn with_formatting(key: &str, value: &str, post_colon_ws: &str, indent: &str) -> Entry {
2331        Self::try_with_formatting(key, value, post_colon_ws, indent)
2332            .expect("Invalid value: empty continuation line")
2333    }
2334
2335    #[must_use]
2336    /// Reformat this entry
2337    ///
2338    /// # Arguments
2339    /// * `indentation` - The indentation to use
2340    /// * `immediate_empty_line` - Whether multi-line values should always start with an empty line
2341    /// * `max_line_length_one_liner` - If set, then this is the max length of the value if it is
2342    ///   crammed into a "one-liner" value
2343    /// * `format_value` - If set, then this function will format the value according to the given
2344    ///   function
2345    ///
2346    /// # Returns
2347    /// The reformatted entry
2348    pub fn wrap_and_sort(
2349        &self,
2350        mut indentation: Indentation,
2351        immediate_empty_line: bool,
2352        max_line_length_one_liner: Option<usize>,
2353        format_value: Option<&dyn Fn(&str, &str) -> String>,
2354    ) -> Entry {
2355        let mut builder = GreenNodeBuilder::new();
2356
2357        let mut content = vec![];
2358        builder.start_node(ENTRY.into());
2359        for c in self.0.children_with_tokens() {
2360            let text = c.as_token().map(|t| t.text());
2361            match c.kind() {
2362                KEY => {
2363                    builder.token(KEY.into(), text.unwrap());
2364                    if indentation == Indentation::FieldNameLength {
2365                        indentation = Indentation::Spaces(text.unwrap().len() as u32);
2366                    }
2367                }
2368                COLON => {
2369                    builder.token(COLON.into(), ":");
2370                }
2371                INDENT => {
2372                    // Discard original whitespace
2373                }
2374                ERROR | COMMENT | VALUE | WHITESPACE | NEWLINE => {
2375                    content.push(c);
2376                }
2377                EMPTY_LINE | ENTRY | ROOT | PARAGRAPH => unreachable!(),
2378            }
2379        }
2380
2381        let indentation = if let crate::Indentation::Spaces(i) = indentation {
2382            i
2383        } else {
2384            1
2385        };
2386
2387        assert!(indentation > 0);
2388
2389        // Strip trailing whitespace and newlines
2390        while let Some(c) = content.last() {
2391            if c.kind() == NEWLINE || c.kind() == WHITESPACE {
2392                content.pop();
2393            } else {
2394                break;
2395            }
2396        }
2397
2398        // Reformat iff there is a format function and the value
2399        // has no errors or comments
2400        let tokens = if let Some(ref format_value) = format_value {
2401            if !content
2402                .iter()
2403                .any(|c| c.kind() == ERROR || c.kind() == COMMENT)
2404            {
2405                let concat = content
2406                    .iter()
2407                    .filter_map(|c| c.as_token().map(|t| t.text()))
2408                    .collect::<String>();
2409                let formatted = format_value(self.key().as_ref().unwrap(), &concat);
2410                crate::lex::lex_inline(&formatted)
2411                    .map(|(k, t)| (k, t.to_string()))
2412                    .collect::<Vec<_>>()
2413            } else {
2414                content
2415                    .into_iter()
2416                    .map(|n| n.into_token().unwrap())
2417                    .map(|i| (i.kind(), i.text().to_string()))
2418                    .collect::<Vec<_>>()
2419            }
2420        } else {
2421            content
2422                .into_iter()
2423                .map(|n| n.into_token().unwrap())
2424                .map(|i| (i.kind(), i.text().to_string()))
2425                .collect::<Vec<_>>()
2426        };
2427
2428        rebuild_value(
2429            &mut builder,
2430            tokens,
2431            self.key().map_or(0, |k| k.len()),
2432            indentation,
2433            immediate_empty_line,
2434            max_line_length_one_liner,
2435        );
2436
2437        builder.finish_node();
2438        Self(SyntaxNode::new_root_mut(builder.finish()))
2439    }
2440
2441    /// Returns the key of the entry.
2442    pub fn key(&self) -> Option<String> {
2443        self.0
2444            .children_with_tokens()
2445            .filter_map(|it| it.into_token())
2446            .find(|it| it.kind() == KEY)
2447            .map(|it| it.text().to_string())
2448    }
2449
2450    /// Returns the value of the entry.
2451    pub fn value(&self) -> String {
2452        let mut parts = self
2453            .0
2454            .children_with_tokens()
2455            .filter_map(|it| it.into_token())
2456            .filter(|it| it.kind() == VALUE)
2457            .map(|it| it.text().to_string());
2458
2459        match parts.next() {
2460            None => String::new(),
2461            Some(first) => {
2462                let mut result = first;
2463                for part in parts {
2464                    result.push('\n');
2465                    result.push_str(&part);
2466                }
2467                result
2468            }
2469        }
2470    }
2471
2472    /// Returns the indentation string used for continuation lines in this entry.
2473    /// Returns None if the entry has no continuation lines.
2474    fn get_indent(&self) -> Option<String> {
2475        self.0
2476            .children_with_tokens()
2477            .filter_map(|it| it.into_token())
2478            .find(|it| it.kind() == INDENT)
2479            .map(|it| it.text().to_string())
2480    }
2481
2482    /// Returns the whitespace immediately after the colon in this entry.
2483    /// This includes WHITESPACE, NEWLINE, and INDENT tokens up to the first VALUE token.
2484    /// Returns None if there is no whitespace (which would be malformed).
2485    fn get_post_colon_whitespace(&self) -> Option<String> {
2486        let mut found_colon = false;
2487        let mut whitespace = String::new();
2488
2489        for token in self
2490            .0
2491            .children_with_tokens()
2492            .filter_map(|it| it.into_token())
2493        {
2494            if token.kind() == COLON {
2495                found_colon = true;
2496                continue;
2497            }
2498
2499            if found_colon {
2500                if token.kind() == WHITESPACE || token.kind() == NEWLINE || token.kind() == INDENT {
2501                    whitespace.push_str(token.text());
2502                } else {
2503                    // We've reached a non-whitespace token, stop collecting
2504                    break;
2505                }
2506            }
2507        }
2508
2509        if whitespace.is_empty() {
2510            None
2511        } else {
2512            Some(whitespace)
2513        }
2514    }
2515
2516    /// Normalize the spacing around the field separator (colon) in place.
2517    ///
2518    /// This ensures that there is exactly one space after the colon and before the value.
2519    /// This is a lossless operation that preserves the field name and value content,
2520    /// but normalizes the whitespace formatting.
2521    ///
2522    /// # Examples
2523    ///
2524    /// ```
2525    /// use deb822_lossless::Deb822;
2526    /// use std::str::FromStr;
2527    ///
2528    /// // Parse an entry with extra spacing after the colon
2529    /// let input = "Field:    value\n";
2530    /// let mut deb822 = Deb822::from_str(input).unwrap();
2531    /// let mut para = deb822.paragraphs().next().unwrap();
2532    ///
2533    /// para.normalize_field_spacing();
2534    /// assert_eq!(para.get("Field").as_deref(), Some("value"));
2535    /// ```
2536    pub fn normalize_field_spacing(&mut self) -> bool {
2537        use rowan::GreenNodeBuilder;
2538
2539        // Store the original text for comparison
2540        let original_text = self.0.text().to_string();
2541
2542        // Build normalized entry
2543        let mut builder = GreenNodeBuilder::new();
2544        builder.start_node(ENTRY.into());
2545
2546        let mut seen_colon = false;
2547        let mut skip_whitespace = false;
2548
2549        for child in self.0.children_with_tokens() {
2550            match child.kind() {
2551                KEY => {
2552                    builder.token(KEY.into(), child.as_token().unwrap().text());
2553                }
2554                COLON => {
2555                    builder.token(COLON.into(), ":");
2556                    seen_colon = true;
2557                    skip_whitespace = true;
2558                }
2559                WHITESPACE if skip_whitespace => {
2560                    // Skip existing whitespace after colon
2561                    continue;
2562                }
2563                VALUE if skip_whitespace => {
2564                    // Add exactly one space before the first value token
2565                    builder.token(WHITESPACE.into(), " ");
2566                    builder.token(VALUE.into(), child.as_token().unwrap().text());
2567                    skip_whitespace = false;
2568                }
2569                NEWLINE if skip_whitespace && seen_colon => {
2570                    // Empty value case (e.g., "Field:\n" or "Field:  \n")
2571                    // Normalize to no trailing space - just output newline
2572                    builder.token(NEWLINE.into(), "\n");
2573                    skip_whitespace = false;
2574                }
2575                _ => {
2576                    // Copy all other tokens as-is
2577                    if let Some(token) = child.as_token() {
2578                        builder.token(token.kind().into(), token.text());
2579                    }
2580                }
2581            }
2582        }
2583
2584        builder.finish_node();
2585        let normalized_green = builder.finish();
2586        let normalized = SyntaxNode::new_root_mut(normalized_green);
2587
2588        // Check if normalization made any changes
2589        let changed = original_text != normalized.text().to_string();
2590
2591        if changed {
2592            // Replace this entry in place
2593            if let Some(parent) = self.0.parent() {
2594                let index = self.0.index();
2595                parent.splice_children(index..index + 1, vec![normalized.into()]);
2596            }
2597        }
2598
2599        changed
2600    }
2601
2602    /// Detach this entry from the paragraph.
2603    pub fn detach(&mut self) {
2604        self.0.detach();
2605    }
2606}
2607
2608impl FromStr for Deb822 {
2609    type Err = ParseError;
2610
2611    fn from_str(s: &str) -> Result<Self, Self::Err> {
2612        Deb822::parse(s).to_result()
2613    }
2614}
2615
2616#[test]
2617fn test_parse_simple() {
2618    const CONTROLV1: &str = r#"Source: foo
2619Maintainer: Foo Bar <foo@example.com>
2620Section: net
2621
2622# This is a comment
2623
2624Package: foo
2625Architecture: all
2626Depends:
2627 bar,
2628 blah
2629Description: This is a description
2630 And it is
2631 .
2632 multiple
2633 lines
2634"#;
2635    let parsed = parse(CONTROLV1);
2636    let node = parsed.syntax();
2637    assert_eq!(
2638        format!("{:#?}", node),
2639        r###"ROOT@0..203
2640  PARAGRAPH@0..63
2641    ENTRY@0..12
2642      KEY@0..6 "Source"
2643      COLON@6..7 ":"
2644      WHITESPACE@7..8 " "
2645      VALUE@8..11 "foo"
2646      NEWLINE@11..12 "\n"
2647    ENTRY@12..50
2648      KEY@12..22 "Maintainer"
2649      COLON@22..23 ":"
2650      WHITESPACE@23..24 " "
2651      VALUE@24..49 "Foo Bar <foo@example. ..."
2652      NEWLINE@49..50 "\n"
2653    ENTRY@50..63
2654      KEY@50..57 "Section"
2655      COLON@57..58 ":"
2656      WHITESPACE@58..59 " "
2657      VALUE@59..62 "net"
2658      NEWLINE@62..63 "\n"
2659  EMPTY_LINE@63..64
2660    NEWLINE@63..64 "\n"
2661  EMPTY_LINE@64..84
2662    COMMENT@64..83 "# This is a comment"
2663    NEWLINE@83..84 "\n"
2664  EMPTY_LINE@84..85
2665    NEWLINE@84..85 "\n"
2666  PARAGRAPH@85..203
2667    ENTRY@85..98
2668      KEY@85..92 "Package"
2669      COLON@92..93 ":"
2670      WHITESPACE@93..94 " "
2671      VALUE@94..97 "foo"
2672      NEWLINE@97..98 "\n"
2673    ENTRY@98..116
2674      KEY@98..110 "Architecture"
2675      COLON@110..111 ":"
2676      WHITESPACE@111..112 " "
2677      VALUE@112..115 "all"
2678      NEWLINE@115..116 "\n"
2679    ENTRY@116..137
2680      KEY@116..123 "Depends"
2681      COLON@123..124 ":"
2682      NEWLINE@124..125 "\n"
2683      INDENT@125..126 " "
2684      VALUE@126..130 "bar,"
2685      NEWLINE@130..131 "\n"
2686      INDENT@131..132 " "
2687      VALUE@132..136 "blah"
2688      NEWLINE@136..137 "\n"
2689    ENTRY@137..203
2690      KEY@137..148 "Description"
2691      COLON@148..149 ":"
2692      WHITESPACE@149..150 " "
2693      VALUE@150..171 "This is a description"
2694      NEWLINE@171..172 "\n"
2695      INDENT@172..173 " "
2696      VALUE@173..182 "And it is"
2697      NEWLINE@182..183 "\n"
2698      INDENT@183..184 " "
2699      VALUE@184..185 "."
2700      NEWLINE@185..186 "\n"
2701      INDENT@186..187 " "
2702      VALUE@187..195 "multiple"
2703      NEWLINE@195..196 "\n"
2704      INDENT@196..197 " "
2705      VALUE@197..202 "lines"
2706      NEWLINE@202..203 "\n"
2707"###
2708    );
2709    assert_eq!(parsed.errors, Vec::<String>::new());
2710
2711    let root = parsed.root_mut();
2712    assert_eq!(root.paragraphs().count(), 2);
2713    let source = root.paragraphs().next().unwrap();
2714    assert_eq!(
2715        source.keys().collect::<Vec<_>>(),
2716        vec!["Source", "Maintainer", "Section"]
2717    );
2718    assert_eq!(source.get("Source").as_deref(), Some("foo"));
2719    assert_eq!(
2720        source.get("Maintainer").as_deref(),
2721        Some("Foo Bar <foo@example.com>")
2722    );
2723    assert_eq!(source.get("Section").as_deref(), Some("net"));
2724    assert_eq!(
2725        source.items().collect::<Vec<_>>(),
2726        vec![
2727            ("Source".into(), "foo".into()),
2728            ("Maintainer".into(), "Foo Bar <foo@example.com>".into()),
2729            ("Section".into(), "net".into()),
2730        ]
2731    );
2732
2733    let binary = root.paragraphs().nth(1).unwrap();
2734    assert_eq!(
2735        binary.keys().collect::<Vec<_>>(),
2736        vec!["Package", "Architecture", "Depends", "Description"]
2737    );
2738    assert_eq!(binary.get("Package").as_deref(), Some("foo"));
2739    assert_eq!(binary.get("Architecture").as_deref(), Some("all"));
2740    assert_eq!(binary.get("Depends").as_deref(), Some("bar,\nblah"));
2741    assert_eq!(
2742        binary.get("Description").as_deref(),
2743        Some("This is a description\nAnd it is\n.\nmultiple\nlines")
2744    );
2745
2746    assert_eq!(node.text(), CONTROLV1);
2747}
2748
2749#[test]
2750fn test_with_trailing_whitespace() {
2751    const CONTROLV1: &str = r#"Source: foo
2752Maintainer: Foo Bar <foo@example.com>
2753
2754
2755"#;
2756    let parsed = parse(CONTROLV1);
2757    let node = parsed.syntax();
2758    assert_eq!(
2759        format!("{:#?}", node),
2760        r###"ROOT@0..52
2761  PARAGRAPH@0..50
2762    ENTRY@0..12
2763      KEY@0..6 "Source"
2764      COLON@6..7 ":"
2765      WHITESPACE@7..8 " "
2766      VALUE@8..11 "foo"
2767      NEWLINE@11..12 "\n"
2768    ENTRY@12..50
2769      KEY@12..22 "Maintainer"
2770      COLON@22..23 ":"
2771      WHITESPACE@23..24 " "
2772      VALUE@24..49 "Foo Bar <foo@example. ..."
2773      NEWLINE@49..50 "\n"
2774  EMPTY_LINE@50..51
2775    NEWLINE@50..51 "\n"
2776  EMPTY_LINE@51..52
2777    NEWLINE@51..52 "\n"
2778"###
2779    );
2780    assert_eq!(parsed.errors, Vec::<String>::new());
2781
2782    let root = parsed.root_mut();
2783    assert_eq!(root.paragraphs().count(), 1);
2784    let source = root.paragraphs().next().unwrap();
2785    assert_eq!(
2786        source.items().collect::<Vec<_>>(),
2787        vec![
2788            ("Source".into(), "foo".into()),
2789            ("Maintainer".into(), "Foo Bar <foo@example.com>".into()),
2790        ]
2791    );
2792}
2793
2794fn rebuild_value(
2795    builder: &mut GreenNodeBuilder,
2796    mut tokens: Vec<(SyntaxKind, String)>,
2797    key_len: usize,
2798    indentation: u32,
2799    immediate_empty_line: bool,
2800    max_line_length_one_liner: Option<usize>,
2801) {
2802    let first_line_len = tokens
2803        .iter()
2804        .take_while(|(k, _t)| *k != NEWLINE)
2805        .map(|(_k, t)| t.len())
2806        .sum::<usize>() + key_len + 2 /* ": " */;
2807
2808    let has_newline = tokens.iter().any(|(k, _t)| *k == NEWLINE);
2809
2810    let mut last_was_newline = false;
2811    if max_line_length_one_liner
2812        .map(|mll| first_line_len <= mll)
2813        .unwrap_or(false)
2814        && !has_newline
2815    {
2816        // Just copy tokens if the value fits into one line
2817        for (k, t) in tokens {
2818            builder.token(k.into(), &t);
2819        }
2820    } else {
2821        // Insert a leading newline if the value is multi-line and immediate_empty_line is set
2822        if immediate_empty_line && has_newline {
2823            builder.token(NEWLINE.into(), "\n");
2824            last_was_newline = true;
2825        } else {
2826            builder.token(WHITESPACE.into(), " ");
2827        }
2828        // Strip leading whitespace and newlines
2829        let mut start_idx = 0;
2830        while start_idx < tokens.len() {
2831            if tokens[start_idx].0 == NEWLINE || tokens[start_idx].0 == WHITESPACE {
2832                start_idx += 1;
2833            } else {
2834                break;
2835            }
2836        }
2837        tokens.drain(..start_idx);
2838        // Pre-allocate indentation string to avoid repeated allocations
2839        let indent_str = " ".repeat(indentation as usize);
2840        for (k, t) in tokens {
2841            if last_was_newline {
2842                builder.token(INDENT.into(), &indent_str);
2843            }
2844            builder.token(k.into(), &t);
2845            last_was_newline = k == NEWLINE;
2846        }
2847    }
2848
2849    if !last_was_newline {
2850        builder.token(NEWLINE.into(), "\n");
2851    }
2852}
2853
2854#[cfg(test)]
2855mod tests {
2856    use super::*;
2857    #[test]
2858    fn test_parse() {
2859        let d: super::Deb822 = r#"Source: foo
2860Maintainer: Foo Bar <jelmer@jelmer.uk>
2861Section: net
2862
2863Package: foo
2864Architecture: all
2865Depends: libc6
2866Description: This is a description
2867 With details
2868"#
2869        .parse()
2870        .unwrap();
2871        let mut ps = d.paragraphs();
2872        let p = ps.next().unwrap();
2873
2874        assert_eq!(p.get("Source").as_deref(), Some("foo"));
2875        assert_eq!(
2876            p.get("Maintainer").as_deref(),
2877            Some("Foo Bar <jelmer@jelmer.uk>")
2878        );
2879        assert_eq!(p.get("Section").as_deref(), Some("net"));
2880
2881        let b = ps.next().unwrap();
2882        assert_eq!(b.get("Package").as_deref(), Some("foo"));
2883    }
2884
2885    #[test]
2886    fn test_after_multi_line() {
2887        let d: super::Deb822 = r#"Source: golang-github-blah-blah
2888Section: devel
2889Priority: optional
2890Standards-Version: 4.2.0
2891Maintainer: Some Maintainer <example@example.com>
2892Build-Depends: debhelper (>= 11~),
2893               dh-golang,
2894               golang-any
2895Homepage: https://github.com/j-keck/arping
2896"#
2897        .parse()
2898        .unwrap();
2899        let mut ps = d.paragraphs();
2900        let p = ps.next().unwrap();
2901        assert_eq!(p.get("Source").as_deref(), Some("golang-github-blah-blah"));
2902        assert_eq!(p.get("Section").as_deref(), Some("devel"));
2903        assert_eq!(p.get("Priority").as_deref(), Some("optional"));
2904        assert_eq!(p.get("Standards-Version").as_deref(), Some("4.2.0"));
2905        assert_eq!(
2906            p.get("Maintainer").as_deref(),
2907            Some("Some Maintainer <example@example.com>")
2908        );
2909        assert_eq!(
2910            p.get("Build-Depends").as_deref(),
2911            Some("debhelper (>= 11~),\ndh-golang,\ngolang-any")
2912        );
2913        assert_eq!(
2914            p.get("Homepage").as_deref(),
2915            Some("https://github.com/j-keck/arping")
2916        );
2917    }
2918
2919    #[test]
2920    fn test_remove_field() {
2921        let d: super::Deb822 = r#"Source: foo
2922# Comment
2923Maintainer: Foo Bar <jelmer@jelmer.uk>
2924Section: net
2925
2926Package: foo
2927Architecture: all
2928Depends: libc6
2929Description: This is a description
2930 With details
2931"#
2932        .parse()
2933        .unwrap();
2934        let mut ps = d.paragraphs();
2935        let mut p = ps.next().unwrap();
2936        p.set("Foo", "Bar");
2937        p.remove("Section");
2938        p.remove("Nonexistent");
2939        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
2940        assert_eq!(
2941            p.to_string(),
2942            r#"Source: foo
2943# Comment
2944Maintainer: Foo Bar <jelmer@jelmer.uk>
2945Foo: Bar
2946"#
2947        );
2948    }
2949
2950    #[test]
2951    fn test_rename_field() {
2952        let d: super::Deb822 = r#"Source: foo
2953Vcs-Browser: https://salsa.debian.org/debian/foo
2954"#
2955        .parse()
2956        .unwrap();
2957        let mut ps = d.paragraphs();
2958        let mut p = ps.next().unwrap();
2959        assert!(p.rename("Vcs-Browser", "Homepage"));
2960        assert_eq!(
2961            p.to_string(),
2962            r#"Source: foo
2963Homepage: https://salsa.debian.org/debian/foo
2964"#
2965        );
2966
2967        assert_eq!(
2968            p.get("Homepage").as_deref(),
2969            Some("https://salsa.debian.org/debian/foo")
2970        );
2971        assert_eq!(p.get("Vcs-Browser").as_deref(), None);
2972
2973        // Nonexistent field
2974        assert!(!p.rename("Nonexistent", "Homepage"));
2975    }
2976
2977    #[test]
2978    fn test_set_field() {
2979        let d: super::Deb822 = r#"Source: foo
2980Maintainer: Foo Bar <joe@example.com>
2981"#
2982        .parse()
2983        .unwrap();
2984        let mut ps = d.paragraphs();
2985        let mut p = ps.next().unwrap();
2986        p.set("Maintainer", "Somebody Else <jane@example.com>");
2987        assert_eq!(
2988            p.get("Maintainer").as_deref(),
2989            Some("Somebody Else <jane@example.com>")
2990        );
2991        assert_eq!(
2992            p.to_string(),
2993            r#"Source: foo
2994Maintainer: Somebody Else <jane@example.com>
2995"#
2996        );
2997    }
2998
2999    #[test]
3000    fn test_set_new_field() {
3001        let d: super::Deb822 = r#"Source: foo
3002"#
3003        .parse()
3004        .unwrap();
3005        let mut ps = d.paragraphs();
3006        let mut p = ps.next().unwrap();
3007        p.set("Maintainer", "Somebody <joe@example.com>");
3008        assert_eq!(
3009            p.get("Maintainer").as_deref(),
3010            Some("Somebody <joe@example.com>")
3011        );
3012        assert_eq!(
3013            p.to_string(),
3014            r#"Source: foo
3015Maintainer: Somebody <joe@example.com>
3016"#
3017        );
3018    }
3019
3020    #[test]
3021    fn test_add_paragraph() {
3022        let mut d = super::Deb822::new();
3023        let mut p = d.add_paragraph();
3024        p.set("Foo", "Bar");
3025        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3026        assert_eq!(
3027            p.to_string(),
3028            r#"Foo: Bar
3029"#
3030        );
3031        assert_eq!(
3032            d.to_string(),
3033            r#"Foo: Bar
3034"#
3035        );
3036
3037        let mut p = d.add_paragraph();
3038        p.set("Foo", "Blah");
3039        assert_eq!(p.get("Foo").as_deref(), Some("Blah"));
3040        assert_eq!(
3041            d.to_string(),
3042            r#"Foo: Bar
3043
3044Foo: Blah
3045"#
3046        );
3047    }
3048
3049    #[test]
3050    fn test_crud_paragraph() {
3051        let mut d = super::Deb822::new();
3052        let mut p = d.insert_paragraph(0);
3053        p.set("Foo", "Bar");
3054        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3055        assert_eq!(
3056            d.to_string(),
3057            r#"Foo: Bar
3058"#
3059        );
3060
3061        // test prepend
3062        let mut p = d.insert_paragraph(0);
3063        p.set("Foo", "Blah");
3064        assert_eq!(p.get("Foo").as_deref(), Some("Blah"));
3065        assert_eq!(
3066            d.to_string(),
3067            r#"Foo: Blah
3068
3069Foo: Bar
3070"#
3071        );
3072
3073        // test delete
3074        d.remove_paragraph(1);
3075        assert_eq!(d.to_string(), "Foo: Blah\n\n");
3076
3077        // test update again
3078        p.set("Foo", "Baz");
3079        assert_eq!(d.to_string(), "Foo: Baz\n\n");
3080
3081        // test delete again
3082        d.remove_paragraph(0);
3083        assert_eq!(d.to_string(), "");
3084    }
3085
3086    #[test]
3087    fn test_swap_paragraphs() {
3088        // Test basic swap
3089        let mut d: super::Deb822 = vec![
3090            vec![("Foo", "Bar")].into_iter().collect(),
3091            vec![("A", "B")].into_iter().collect(),
3092            vec![("X", "Y")].into_iter().collect(),
3093        ]
3094        .into_iter()
3095        .collect();
3096
3097        d.swap_paragraphs(0, 2);
3098        assert_eq!(d.to_string(), "X: Y\n\nA: B\n\nFoo: Bar\n");
3099
3100        // Swap back
3101        d.swap_paragraphs(0, 2);
3102        assert_eq!(d.to_string(), "Foo: Bar\n\nA: B\n\nX: Y\n");
3103
3104        // Swap adjacent paragraphs
3105        d.swap_paragraphs(0, 1);
3106        assert_eq!(d.to_string(), "A: B\n\nFoo: Bar\n\nX: Y\n");
3107
3108        // Swap with same index should be no-op
3109        let before = d.to_string();
3110        d.swap_paragraphs(1, 1);
3111        assert_eq!(d.to_string(), before);
3112    }
3113
3114    #[test]
3115    fn test_swap_paragraphs_preserves_content() {
3116        // Test that field content is preserved
3117        let mut d: super::Deb822 = vec![
3118            vec![("Field1", "Value1"), ("Field2", "Value2")]
3119                .into_iter()
3120                .collect(),
3121            vec![("FieldA", "ValueA"), ("FieldB", "ValueB")]
3122                .into_iter()
3123                .collect(),
3124        ]
3125        .into_iter()
3126        .collect();
3127
3128        d.swap_paragraphs(0, 1);
3129
3130        let mut paras = d.paragraphs();
3131        let p1 = paras.next().unwrap();
3132        assert_eq!(p1.get("FieldA").as_deref(), Some("ValueA"));
3133        assert_eq!(p1.get("FieldB").as_deref(), Some("ValueB"));
3134
3135        let p2 = paras.next().unwrap();
3136        assert_eq!(p2.get("Field1").as_deref(), Some("Value1"));
3137        assert_eq!(p2.get("Field2").as_deref(), Some("Value2"));
3138    }
3139
3140    #[test]
3141    #[should_panic(expected = "out of bounds")]
3142    fn test_swap_paragraphs_out_of_bounds() {
3143        let mut d: super::Deb822 = vec![
3144            vec![("Foo", "Bar")].into_iter().collect(),
3145            vec![("A", "B")].into_iter().collect(),
3146        ]
3147        .into_iter()
3148        .collect();
3149
3150        d.swap_paragraphs(0, 5);
3151    }
3152
3153    #[test]
3154    fn test_multiline_entry() {
3155        use super::SyntaxKind::*;
3156        use rowan::ast::AstNode;
3157
3158        let entry = super::Entry::new("foo", "bar\nbaz");
3159        let tokens: Vec<_> = entry
3160            .syntax()
3161            .descendants_with_tokens()
3162            .filter_map(|tok| tok.into_token())
3163            .collect();
3164
3165        assert_eq!("foo: bar\n baz\n", entry.to_string());
3166        assert_eq!("bar\nbaz", entry.value());
3167
3168        assert_eq!(
3169            vec![
3170                (KEY, "foo"),
3171                (COLON, ":"),
3172                (WHITESPACE, " "),
3173                (VALUE, "bar"),
3174                (NEWLINE, "\n"),
3175                (INDENT, " "),
3176                (VALUE, "baz"),
3177                (NEWLINE, "\n"),
3178            ],
3179            tokens
3180                .iter()
3181                .map(|token| (token.kind(), token.text()))
3182                .collect::<Vec<_>>()
3183        );
3184    }
3185
3186    #[test]
3187    fn test_apt_entry() {
3188        let text = r#"Package: cvsd
3189Binary: cvsd
3190Version: 1.0.24
3191Maintainer: Arthur de Jong <adejong@debian.org>
3192Build-Depends: debhelper (>= 9), po-debconf
3193Architecture: any
3194Standards-Version: 3.9.3
3195Format: 3.0 (native)
3196Files:
3197 b7a7d67a02974c52c408fdb5e118406d 890 cvsd_1.0.24.dsc
3198 b73ee40774c3086cb8490cdbb96ac883 258139 cvsd_1.0.24.tar.gz
3199Vcs-Browser: http://arthurdejong.org/viewvc/cvsd/
3200Vcs-Cvs: :pserver:anonymous@arthurdejong.org:/arthur/
3201Checksums-Sha256:
3202 a7bb7a3aacee19cd14ce5c26cb86e348b1608e6f1f6e97c6ea7c58efa440ac43 890 cvsd_1.0.24.dsc
3203 46bc517760c1070ae408693b89603986b53e6f068ae6bdc744e2e830e46b8cba 258139 cvsd_1.0.24.tar.gz
3204Homepage: http://arthurdejong.org/cvsd/
3205Package-List:
3206 cvsd deb vcs optional
3207Directory: pool/main/c/cvsd
3208Priority: source
3209Section: vcs
3210
3211"#;
3212        let d: super::Deb822 = text.parse().unwrap();
3213        let p = d.paragraphs().next().unwrap();
3214        assert_eq!(p.get("Binary").as_deref(), Some("cvsd"));
3215        assert_eq!(p.get("Version").as_deref(), Some("1.0.24"));
3216        assert_eq!(
3217            p.get("Maintainer").as_deref(),
3218            Some("Arthur de Jong <adejong@debian.org>")
3219        );
3220    }
3221
3222    #[test]
3223    fn test_format() {
3224        let d: super::Deb822 = r#"Source: foo
3225Maintainer: Foo Bar <foo@example.com>
3226Section:      net
3227Blah: blah  # comment
3228Multi-Line:
3229  Ahoi!
3230     Matey!
3231
3232"#
3233        .parse()
3234        .unwrap();
3235        let mut ps = d.paragraphs();
3236        let p = ps.next().unwrap();
3237        let result = p.wrap_and_sort(
3238            crate::Indentation::FieldNameLength,
3239            false,
3240            None,
3241            None::<&dyn Fn(&super::Entry, &super::Entry) -> std::cmp::Ordering>,
3242            None,
3243        );
3244        assert_eq!(
3245            result.to_string(),
3246            r#"Source: foo
3247Maintainer: Foo Bar <foo@example.com>
3248Section: net
3249Blah: blah  # comment
3250Multi-Line: Ahoi!
3251          Matey!
3252"#
3253        );
3254    }
3255
3256    #[test]
3257    fn test_format_sort_paragraphs() {
3258        let d: super::Deb822 = r#"Source: foo
3259Maintainer: Foo Bar <foo@example.com>
3260
3261# This is a comment
3262Source: bar
3263Maintainer: Bar Foo <bar@example.com>
3264
3265"#
3266        .parse()
3267        .unwrap();
3268        let result = d.wrap_and_sort(
3269            Some(&|a: &super::Paragraph, b: &super::Paragraph| {
3270                a.get("Source").cmp(&b.get("Source"))
3271            }),
3272            Some(&|p| {
3273                p.wrap_and_sort(
3274                    crate::Indentation::FieldNameLength,
3275                    false,
3276                    None,
3277                    None::<&dyn Fn(&super::Entry, &super::Entry) -> std::cmp::Ordering>,
3278                    None,
3279                )
3280            }),
3281        );
3282        assert_eq!(
3283            result.to_string(),
3284            r#"# This is a comment
3285Source: bar
3286Maintainer: Bar Foo <bar@example.com>
3287
3288Source: foo
3289Maintainer: Foo Bar <foo@example.com>
3290"#,
3291        );
3292    }
3293
3294    #[test]
3295    fn test_format_sort_fields() {
3296        let d: super::Deb822 = r#"Source: foo
3297Maintainer: Foo Bar <foo@example.com>
3298Build-Depends: debhelper (>= 9), po-debconf
3299Homepage: https://example.com/
3300
3301"#
3302        .parse()
3303        .unwrap();
3304        let result = d.wrap_and_sort(
3305            None,
3306            Some(&|p: &super::Paragraph| -> super::Paragraph {
3307                p.wrap_and_sort(
3308                    crate::Indentation::FieldNameLength,
3309                    false,
3310                    None,
3311                    Some(&|a: &super::Entry, b: &super::Entry| a.key().cmp(&b.key())),
3312                    None,
3313                )
3314            }),
3315        );
3316        assert_eq!(
3317            result.to_string(),
3318            r#"Build-Depends: debhelper (>= 9), po-debconf
3319Homepage: https://example.com/
3320Maintainer: Foo Bar <foo@example.com>
3321Source: foo
3322"#
3323        );
3324    }
3325
3326    #[test]
3327    fn test_para_from_iter() {
3328        let p: super::Paragraph = vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect();
3329        assert_eq!(
3330            p.to_string(),
3331            r#"Foo: Bar
3332Baz: Qux
3333"#
3334        );
3335
3336        let p: super::Paragraph = vec![
3337            ("Foo".to_string(), "Bar".to_string()),
3338            ("Baz".to_string(), "Qux".to_string()),
3339        ]
3340        .into_iter()
3341        .collect();
3342
3343        assert_eq!(
3344            p.to_string(),
3345            r#"Foo: Bar
3346Baz: Qux
3347"#
3348        );
3349    }
3350
3351    #[test]
3352    fn test_deb822_from_iter() {
3353        let d: super::Deb822 = vec![
3354            vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
3355            vec![("A", "B"), ("C", "D")].into_iter().collect(),
3356        ]
3357        .into_iter()
3358        .collect();
3359        assert_eq!(
3360            d.to_string(),
3361            r#"Foo: Bar
3362Baz: Qux
3363
3364A: B
3365C: D
3366"#
3367        );
3368    }
3369
3370    #[test]
3371    fn test_format_parse_error() {
3372        assert_eq!(ParseError(vec!["foo".to_string()]).to_string(), "foo\n");
3373    }
3374
3375    #[test]
3376    fn test_set_with_field_order() {
3377        let mut p = super::Paragraph::new();
3378        let custom_order = &["Foo", "Bar", "Baz"];
3379
3380        p.set_with_field_order("Baz", "3", custom_order);
3381        p.set_with_field_order("Foo", "1", custom_order);
3382        p.set_with_field_order("Bar", "2", custom_order);
3383        p.set_with_field_order("Unknown", "4", custom_order);
3384
3385        let keys: Vec<_> = p.keys().collect();
3386        assert_eq!(keys[0], "Foo");
3387        assert_eq!(keys[1], "Bar");
3388        assert_eq!(keys[2], "Baz");
3389        assert_eq!(keys[3], "Unknown");
3390    }
3391
3392    #[test]
3393    fn test_positioned_parse_error() {
3394        let error = PositionedParseError {
3395            message: "test error".to_string(),
3396            range: rowan::TextRange::new(rowan::TextSize::from(5), rowan::TextSize::from(10)),
3397            code: Some("test_code".to_string()),
3398        };
3399        assert_eq!(error.to_string(), "test error");
3400        assert_eq!(error.range.start(), rowan::TextSize::from(5));
3401        assert_eq!(error.range.end(), rowan::TextSize::from(10));
3402        assert_eq!(error.code, Some("test_code".to_string()));
3403    }
3404
3405    #[test]
3406    fn test_format_error() {
3407        assert_eq!(
3408            super::Error::ParseError(ParseError(vec!["foo".to_string()])).to_string(),
3409            "foo\n"
3410        );
3411    }
3412
3413    #[test]
3414    fn test_get_all() {
3415        let d: super::Deb822 = r#"Source: foo
3416Maintainer: Foo Bar <foo@example.com>
3417Maintainer: Bar Foo <bar@example.com>"#
3418            .parse()
3419            .unwrap();
3420        let p = d.paragraphs().next().unwrap();
3421        assert_eq!(
3422            p.get_all("Maintainer").collect::<Vec<_>>(),
3423            vec!["Foo Bar <foo@example.com>", "Bar Foo <bar@example.com>"]
3424        );
3425    }
3426
3427    #[test]
3428    fn test_get_with_indent_single_line() {
3429        let input = "Field: single line value\n";
3430        let deb = super::Deb822::from_str(input).unwrap();
3431        let para = deb.paragraphs().next().unwrap();
3432
3433        // Single-line values should be unchanged regardless of indent pattern
3434        assert_eq!(
3435            para.get_with_indent("Field", &super::IndentPattern::Fixed(2)),
3436            Some("single line value".to_string())
3437        );
3438        assert_eq!(
3439            para.get_with_indent("Field", &super::IndentPattern::FieldNameLength),
3440            Some("single line value".to_string())
3441        );
3442    }
3443
3444    #[test]
3445    fn test_get_with_indent_fixed() {
3446        let input = "Field: First\n   Second\n   Third\n";
3447        let deb = super::Deb822::from_str(input).unwrap();
3448        let para = deb.paragraphs().next().unwrap();
3449
3450        // Get with fixed 2-space indentation - strips 2 spaces, leaves 1
3451        let value = para
3452            .get_with_indent("Field", &super::IndentPattern::Fixed(2))
3453            .unwrap();
3454        assert_eq!(value, "First\n Second\n Third");
3455
3456        // Get with fixed 1-space indentation - strips 1 space, leaves 2
3457        let value = para
3458            .get_with_indent("Field", &super::IndentPattern::Fixed(1))
3459            .unwrap();
3460        assert_eq!(value, "First\n  Second\n  Third");
3461
3462        // Get with fixed 3-space indentation - strips all 3 spaces
3463        let value = para
3464            .get_with_indent("Field", &super::IndentPattern::Fixed(3))
3465            .unwrap();
3466        assert_eq!(value, "First\nSecond\nThird");
3467    }
3468
3469    #[test]
3470    fn test_get_with_indent_field_name_length() {
3471        let input = "Description: First line\n             Second line\n             Third line\n";
3472        let deb = super::Deb822::from_str(input).unwrap();
3473        let para = deb.paragraphs().next().unwrap();
3474
3475        // Get with FieldNameLength pattern
3476        // "Description: " is 13 characters, so strips 13 spaces, leaves 0
3477        let value = para
3478            .get_with_indent("Description", &super::IndentPattern::FieldNameLength)
3479            .unwrap();
3480        assert_eq!(value, "First line\nSecond line\nThird line");
3481
3482        // Get with fixed 2-space indentation - strips 2, leaves 11
3483        let value = para
3484            .get_with_indent("Description", &super::IndentPattern::Fixed(2))
3485            .unwrap();
3486        assert_eq!(
3487            value,
3488            "First line\n           Second line\n           Third line"
3489        );
3490    }
3491
3492    #[test]
3493    fn test_get_with_indent_nonexistent() {
3494        let input = "Field: value\n";
3495        let deb = super::Deb822::from_str(input).unwrap();
3496        let para = deb.paragraphs().next().unwrap();
3497
3498        assert_eq!(
3499            para.get_with_indent("NonExistent", &super::IndentPattern::Fixed(2)),
3500            None
3501        );
3502    }
3503
3504    #[test]
3505    fn test_get_entry() {
3506        let input = r#"Package: test-package
3507Maintainer: Test User <test@example.com>
3508Description: A simple test package
3509 with multiple lines
3510"#;
3511        let deb = super::Deb822::from_str(input).unwrap();
3512        let para = deb.paragraphs().next().unwrap();
3513
3514        // Test getting existing entry
3515        let entry = para.get_entry("Package");
3516        assert!(entry.is_some());
3517        let entry = entry.unwrap();
3518        assert_eq!(entry.key(), Some("Package".to_string()));
3519        assert_eq!(entry.value(), "test-package");
3520
3521        // Test case-insensitive lookup
3522        let entry = para.get_entry("package");
3523        assert!(entry.is_some());
3524        assert_eq!(entry.unwrap().value(), "test-package");
3525
3526        // Test multi-line value
3527        let entry = para.get_entry("Description");
3528        assert!(entry.is_some());
3529        assert_eq!(
3530            entry.unwrap().value(),
3531            "A simple test package\nwith multiple lines"
3532        );
3533
3534        // Test non-existent field
3535        assert_eq!(para.get_entry("NonExistent"), None);
3536    }
3537
3538    #[test]
3539    fn test_entry_ranges() {
3540        let input = r#"Package: test-package
3541Maintainer: Test User <test@example.com>
3542Description: A simple test package
3543 with multiple lines
3544 of description text"#;
3545
3546        let deb822 = super::Deb822::from_str(input).unwrap();
3547        let paragraph = deb822.paragraphs().next().unwrap();
3548        let entries: Vec<_> = paragraph.entries().collect();
3549
3550        // Test first entry (Package)
3551        let package_entry = &entries[0];
3552        assert_eq!(package_entry.key(), Some("Package".to_string()));
3553
3554        // Test key_range
3555        let key_range = package_entry.key_range().unwrap();
3556        assert_eq!(
3557            &input[key_range.start().into()..key_range.end().into()],
3558            "Package"
3559        );
3560
3561        // Test colon_range
3562        let colon_range = package_entry.colon_range().unwrap();
3563        assert_eq!(
3564            &input[colon_range.start().into()..colon_range.end().into()],
3565            ":"
3566        );
3567
3568        // Test value_range
3569        let value_range = package_entry.value_range().unwrap();
3570        assert_eq!(
3571            &input[value_range.start().into()..value_range.end().into()],
3572            "test-package"
3573        );
3574
3575        // Test text_range covers the whole entry
3576        let text_range = package_entry.text_range();
3577        assert_eq!(
3578            &input[text_range.start().into()..text_range.end().into()],
3579            "Package: test-package\n"
3580        );
3581
3582        // Test single-line value_line_ranges
3583        let value_lines = package_entry.value_line_ranges();
3584        assert_eq!(value_lines.len(), 1);
3585        assert_eq!(
3586            &input[value_lines[0].start().into()..value_lines[0].end().into()],
3587            "test-package"
3588        );
3589    }
3590
3591    #[test]
3592    fn test_multiline_entry_ranges() {
3593        let input = r#"Description: Short description
3594 Extended description line 1
3595 Extended description line 2"#;
3596
3597        let deb822 = super::Deb822::from_str(input).unwrap();
3598        let paragraph = deb822.paragraphs().next().unwrap();
3599        let entry = paragraph.entries().next().unwrap();
3600
3601        assert_eq!(entry.key(), Some("Description".to_string()));
3602
3603        // Test value_range spans all lines
3604        let value_range = entry.value_range().unwrap();
3605        let full_value = &input[value_range.start().into()..value_range.end().into()];
3606        assert!(full_value.contains("Short description"));
3607        assert!(full_value.contains("Extended description line 1"));
3608        assert!(full_value.contains("Extended description line 2"));
3609
3610        // Test value_line_ranges gives individual lines
3611        let value_lines = entry.value_line_ranges();
3612        assert_eq!(value_lines.len(), 3);
3613
3614        assert_eq!(
3615            &input[value_lines[0].start().into()..value_lines[0].end().into()],
3616            "Short description"
3617        );
3618        assert_eq!(
3619            &input[value_lines[1].start().into()..value_lines[1].end().into()],
3620            "Extended description line 1"
3621        );
3622        assert_eq!(
3623            &input[value_lines[2].start().into()..value_lines[2].end().into()],
3624            "Extended description line 2"
3625        );
3626    }
3627
3628    #[test]
3629    fn test_entries_public_access() {
3630        let input = r#"Package: test
3631Version: 1.0"#;
3632
3633        let deb822 = super::Deb822::from_str(input).unwrap();
3634        let paragraph = deb822.paragraphs().next().unwrap();
3635
3636        // Test that entries() method is now public
3637        let entries: Vec<_> = paragraph.entries().collect();
3638        assert_eq!(entries.len(), 2);
3639        assert_eq!(entries[0].key(), Some("Package".to_string()));
3640        assert_eq!(entries[1].key(), Some("Version".to_string()));
3641    }
3642
3643    #[test]
3644    fn test_empty_value_ranges() {
3645        let input = r#"EmptyField: "#;
3646
3647        let deb822 = super::Deb822::from_str(input).unwrap();
3648        let paragraph = deb822.paragraphs().next().unwrap();
3649        let entry = paragraph.entries().next().unwrap();
3650
3651        assert_eq!(entry.key(), Some("EmptyField".to_string()));
3652
3653        // Empty value should still have ranges
3654        assert!(entry.key_range().is_some());
3655        assert!(entry.colon_range().is_some());
3656
3657        // Empty value might not have value tokens
3658        let value_lines = entry.value_line_ranges();
3659        // This depends on how the parser handles empty values
3660        // but we should not panic
3661        assert!(value_lines.len() <= 1);
3662    }
3663
3664    #[test]
3665    fn test_range_ordering() {
3666        let input = r#"Field: value"#;
3667
3668        let deb822 = super::Deb822::from_str(input).unwrap();
3669        let paragraph = deb822.paragraphs().next().unwrap();
3670        let entry = paragraph.entries().next().unwrap();
3671
3672        let key_range = entry.key_range().unwrap();
3673        let colon_range = entry.colon_range().unwrap();
3674        let value_range = entry.value_range().unwrap();
3675        let text_range = entry.text_range();
3676
3677        // Verify ranges are in correct order
3678        assert!(key_range.end() <= colon_range.start());
3679        assert!(colon_range.end() <= value_range.start());
3680        assert!(key_range.start() >= text_range.start());
3681        assert!(value_range.end() <= text_range.end());
3682    }
3683
3684    #[test]
3685    fn test_error_recovery_missing_colon() {
3686        let input = r#"Source foo
3687Maintainer: Test User <test@example.com>
3688"#;
3689        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3690
3691        // Should still parse successfully with errors
3692        assert!(!errors.is_empty());
3693        assert!(errors.iter().any(|e| e.contains("missing colon")));
3694
3695        // Should still have a paragraph with the valid field
3696        let paragraph = deb822.paragraphs().next().unwrap();
3697        assert_eq!(
3698            paragraph.get("Maintainer").as_deref(),
3699            Some("Test User <test@example.com>")
3700        );
3701    }
3702
3703    #[test]
3704    fn test_error_recovery_missing_field_name() {
3705        let input = r#": orphaned value
3706Package: test
3707"#;
3708
3709        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3710
3711        // Should have errors about missing field name
3712        assert!(!errors.is_empty());
3713        assert!(errors
3714            .iter()
3715            .any(|e| e.contains("field name") || e.contains("missing")));
3716
3717        // The valid field should be in one of the paragraphs
3718        let paragraphs: Vec<_> = deb822.paragraphs().collect();
3719        let mut found_package = false;
3720        for paragraph in paragraphs.iter() {
3721            if paragraph.get("Package").is_some() {
3722                found_package = true;
3723                assert_eq!(paragraph.get("Package").as_deref(), Some("test"));
3724            }
3725        }
3726        assert!(found_package, "Package field not found in any paragraph");
3727    }
3728
3729    #[test]
3730    fn test_error_recovery_orphaned_text() {
3731        let input = r#"Package: test
3732some orphaned text without field name
3733Version: 1.0
3734"#;
3735        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3736
3737        // Should have errors about orphaned text
3738        assert!(!errors.is_empty());
3739        assert!(errors.iter().any(|e| e.contains("orphaned")
3740            || e.contains("unexpected")
3741            || e.contains("field name")));
3742
3743        // Should still parse the valid fields (may be split across paragraphs)
3744        let mut all_fields = std::collections::HashMap::new();
3745        for paragraph in deb822.paragraphs() {
3746            for (key, value) in paragraph.items() {
3747                all_fields.insert(key, value);
3748            }
3749        }
3750
3751        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
3752        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
3753    }
3754
3755    #[test]
3756    fn test_error_recovery_consecutive_field_names() {
3757        let input = r#"Package: test
3758Description
3759Maintainer: Another field without proper value
3760Version: 1.0
3761"#;
3762        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3763
3764        // Should have errors about missing values
3765        assert!(!errors.is_empty());
3766        assert!(errors.iter().any(|e| e.contains("consecutive")
3767            || e.contains("missing")
3768            || e.contains("incomplete")));
3769
3770        // Should still parse valid fields (may be split across paragraphs due to errors)
3771        let mut all_fields = std::collections::HashMap::new();
3772        for paragraph in deb822.paragraphs() {
3773            for (key, value) in paragraph.items() {
3774                all_fields.insert(key, value);
3775            }
3776        }
3777
3778        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
3779        assert_eq!(
3780            all_fields.get("Maintainer"),
3781            Some(&"Another field without proper value".to_string())
3782        );
3783        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
3784    }
3785
3786    #[test]
3787    fn test_error_recovery_malformed_multiline() {
3788        let input = r#"Package: test
3789Description: Short desc
3790  Proper continuation
3791invalid continuation without indent
3792 Another proper continuation
3793Version: 1.0
3794"#;
3795        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3796
3797        // Should recover from malformed continuation
3798        assert!(!errors.is_empty());
3799
3800        // Should still parse other fields correctly
3801        let paragraph = deb822.paragraphs().next().unwrap();
3802        assert_eq!(paragraph.get("Package").as_deref(), Some("test"));
3803        assert_eq!(paragraph.get("Version").as_deref(), Some("1.0"));
3804    }
3805
3806    #[test]
3807    fn test_error_recovery_mixed_errors() {
3808        let input = r#"Package test without colon
3809: orphaned colon
3810Description: Valid field
3811some orphaned text
3812Another-Field: Valid too
3813"#;
3814        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3815
3816        // Should have multiple different errors
3817        assert!(!errors.is_empty());
3818        assert!(errors.len() >= 2);
3819
3820        // Should still parse the valid fields
3821        let paragraph = deb822.paragraphs().next().unwrap();
3822        assert_eq!(paragraph.get("Description").as_deref(), Some("Valid field"));
3823        assert_eq!(paragraph.get("Another-Field").as_deref(), Some("Valid too"));
3824    }
3825
3826    #[test]
3827    fn test_error_recovery_paragraph_boundary() {
3828        let input = r#"Package: first-package
3829Description: First paragraph
3830
3831corrupted data here
3832: more corruption
3833completely broken line
3834
3835Package: second-package
3836Version: 1.0
3837"#;
3838        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3839
3840        // Should have errors from the corrupted section
3841        assert!(!errors.is_empty());
3842
3843        // Should still parse both paragraphs correctly
3844        let paragraphs: Vec<_> = deb822.paragraphs().collect();
3845        assert_eq!(paragraphs.len(), 2);
3846
3847        assert_eq!(
3848            paragraphs[0].get("Package").as_deref(),
3849            Some("first-package")
3850        );
3851        assert_eq!(
3852            paragraphs[1].get("Package").as_deref(),
3853            Some("second-package")
3854        );
3855        assert_eq!(paragraphs[1].get("Version").as_deref(), Some("1.0"));
3856    }
3857
3858    #[test]
3859    fn test_error_recovery_with_positioned_errors() {
3860        let input = r#"Package test
3861Description: Valid
3862"#;
3863        let parsed = super::parse(input);
3864
3865        // Should have positioned errors with proper ranges
3866        assert!(!parsed.positioned_errors.is_empty());
3867
3868        let first_error = &parsed.positioned_errors[0];
3869        assert!(!first_error.message.is_empty());
3870        assert!(first_error.range.start() <= first_error.range.end());
3871        assert!(first_error.code.is_some());
3872
3873        // Error should point to the problematic location
3874        let error_text = &input[first_error.range.start().into()..first_error.range.end().into()];
3875        assert!(!error_text.is_empty());
3876    }
3877
3878    #[test]
3879    fn test_error_recovery_preserves_whitespace() {
3880        let input = r#"Source: package
3881Maintainer   Test User <test@example.com>
3882Section:    utils
3883
3884"#;
3885        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3886
3887        // Should have error about missing colon
3888        assert!(!errors.is_empty());
3889
3890        // Should preserve original formatting in output
3891        let output = deb822.to_string();
3892        assert!(output.contains("Section:    utils"));
3893
3894        // Should still extract valid fields
3895        let paragraph = deb822.paragraphs().next().unwrap();
3896        assert_eq!(paragraph.get("Source").as_deref(), Some("package"));
3897        assert_eq!(paragraph.get("Section").as_deref(), Some("utils"));
3898    }
3899
3900    #[test]
3901    fn test_error_recovery_empty_fields() {
3902        let input = r#"Package: test
3903Description:
3904Maintainer: Valid User
3905EmptyField:
3906Version: 1.0
3907"#;
3908        let (deb822, _errors) = super::Deb822::from_str_relaxed(input);
3909
3910        // Empty fields should parse without major errors - collect all fields from all paragraphs
3911        let mut all_fields = std::collections::HashMap::new();
3912        for paragraph in deb822.paragraphs() {
3913            for (key, value) in paragraph.items() {
3914                all_fields.insert(key, value);
3915            }
3916        }
3917
3918        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
3919        assert_eq!(all_fields.get("Description"), Some(&"".to_string()));
3920        assert_eq!(
3921            all_fields.get("Maintainer"),
3922            Some(&"Valid User".to_string())
3923        );
3924        assert_eq!(all_fields.get("EmptyField"), Some(&"".to_string()));
3925        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
3926    }
3927
3928    #[test]
3929    fn test_insert_comment_before() {
3930        let d: super::Deb822 = vec![
3931            vec![("Source", "foo"), ("Maintainer", "Bar <bar@example.com>")]
3932                .into_iter()
3933                .collect(),
3934            vec![("Package", "foo"), ("Architecture", "all")]
3935                .into_iter()
3936                .collect(),
3937        ]
3938        .into_iter()
3939        .collect();
3940
3941        // Insert comment before first paragraph
3942        let mut p1 = d.paragraphs().next().unwrap();
3943        p1.insert_comment_before("This is the source paragraph");
3944
3945        // Insert comment before second paragraph
3946        let mut p2 = d.paragraphs().nth(1).unwrap();
3947        p2.insert_comment_before("This is the binary paragraph");
3948
3949        let output = d.to_string();
3950        assert_eq!(
3951            output,
3952            r#"# This is the source paragraph
3953Source: foo
3954Maintainer: Bar <bar@example.com>
3955
3956# This is the binary paragraph
3957Package: foo
3958Architecture: all
3959"#
3960        );
3961    }
3962
3963    #[test]
3964    fn test_parse_continuation_with_colon() {
3965        // Test that continuation lines with colons are properly parsed
3966        let input = "Package: test\nDescription: short\n line: with colon\n";
3967        let result = input.parse::<Deb822>();
3968        assert!(result.is_ok());
3969
3970        let deb822 = result.unwrap();
3971        let para = deb822.paragraphs().next().unwrap();
3972        assert_eq!(para.get("Package").as_deref(), Some("test"));
3973        assert_eq!(
3974            para.get("Description").as_deref(),
3975            Some("short\nline: with colon")
3976        );
3977    }
3978
3979    #[test]
3980    fn test_parse_continuation_starting_with_colon() {
3981        // Test continuation line STARTING with a colon (issue #315)
3982        let input = "Package: test\nDescription: short\n :value\n";
3983        let result = input.parse::<Deb822>();
3984        assert!(result.is_ok());
3985
3986        let deb822 = result.unwrap();
3987        let para = deb822.paragraphs().next().unwrap();
3988        assert_eq!(para.get("Package").as_deref(), Some("test"));
3989        assert_eq!(para.get("Description").as_deref(), Some("short\n:value"));
3990    }
3991
3992    #[test]
3993    fn test_normalize_field_spacing_single_space() {
3994        // Field already has correct spacing
3995        let input = "Field: value\n";
3996        let deb822 = input.parse::<Deb822>().unwrap();
3997        let mut para = deb822.paragraphs().next().unwrap();
3998
3999        para.normalize_field_spacing();
4000        assert_eq!(para.to_string(), "Field: value\n");
4001    }
4002
4003    #[test]
4004    fn test_normalize_field_spacing_extra_spaces() {
4005        // Field has extra spaces after colon
4006        let input = "Field:    value\n";
4007        let deb822 = input.parse::<Deb822>().unwrap();
4008        let mut para = deb822.paragraphs().next().unwrap();
4009
4010        para.normalize_field_spacing();
4011        assert_eq!(para.to_string(), "Field: value\n");
4012    }
4013
4014    #[test]
4015    fn test_normalize_field_spacing_no_space() {
4016        // Field has no space after colon
4017        let input = "Field:value\n";
4018        let deb822 = input.parse::<Deb822>().unwrap();
4019        let mut para = deb822.paragraphs().next().unwrap();
4020
4021        para.normalize_field_spacing();
4022        assert_eq!(para.to_string(), "Field: value\n");
4023    }
4024
4025    #[test]
4026    fn test_normalize_field_spacing_multiple_fields() {
4027        // Multiple fields with various spacing
4028        let input = "Field1:    value1\nField2:value2\nField3:  value3\n";
4029        let deb822 = input.parse::<Deb822>().unwrap();
4030        let mut para = deb822.paragraphs().next().unwrap();
4031
4032        para.normalize_field_spacing();
4033        assert_eq!(
4034            para.to_string(),
4035            "Field1: value1\nField2: value2\nField3: value3\n"
4036        );
4037    }
4038
4039    #[test]
4040    fn test_normalize_field_spacing_multiline_value() {
4041        // Field with multiline value
4042        let input = "Description:    short\n continuation line\n .  \n final line\n";
4043        let deb822 = input.parse::<Deb822>().unwrap();
4044        let mut para = deb822.paragraphs().next().unwrap();
4045
4046        para.normalize_field_spacing();
4047        assert_eq!(
4048            para.to_string(),
4049            "Description: short\n continuation line\n .  \n final line\n"
4050        );
4051    }
4052
4053    #[test]
4054    fn test_normalize_field_spacing_empty_value_with_whitespace() {
4055        // Field with empty value (only whitespace) should normalize to no space
4056        let input = "Field:  \n";
4057        let deb822 = input.parse::<Deb822>().unwrap();
4058        let mut para = deb822.paragraphs().next().unwrap();
4059
4060        para.normalize_field_spacing();
4061        // When value is empty/whitespace-only, normalize to no space
4062        assert_eq!(para.to_string(), "Field:\n");
4063    }
4064
4065    #[test]
4066    fn test_normalize_field_spacing_no_value() {
4067        // Field with no value (just newline) should stay unchanged
4068        let input = "Depends:\n";
4069        let deb822 = input.parse::<Deb822>().unwrap();
4070        let mut para = deb822.paragraphs().next().unwrap();
4071
4072        para.normalize_field_spacing();
4073        // Should remain with no space
4074        assert_eq!(para.to_string(), "Depends:\n");
4075    }
4076
4077    #[test]
4078    fn test_normalize_field_spacing_multiple_paragraphs() {
4079        // Multiple paragraphs
4080        let input = "Field1:    value1\n\nField2:  value2\n";
4081        let mut deb822 = input.parse::<Deb822>().unwrap();
4082
4083        deb822.normalize_field_spacing();
4084        assert_eq!(deb822.to_string(), "Field1: value1\n\nField2: value2\n");
4085    }
4086
4087    #[test]
4088    fn test_normalize_field_spacing_preserves_comments() {
4089        // Normalize spacing while preserving comments (comments are at document level)
4090        let input = "# Comment\nField:    value\n";
4091        let mut deb822 = input.parse::<Deb822>().unwrap();
4092
4093        deb822.normalize_field_spacing();
4094        assert_eq!(deb822.to_string(), "# Comment\nField: value\n");
4095    }
4096
4097    #[test]
4098    fn test_normalize_field_spacing_preserves_values() {
4099        // Ensure values are preserved exactly
4100        let input = "Source:   foo-bar\nMaintainer:Foo Bar <test@example.com>\n";
4101        let deb822 = input.parse::<Deb822>().unwrap();
4102        let mut para = deb822.paragraphs().next().unwrap();
4103
4104        para.normalize_field_spacing();
4105
4106        assert_eq!(para.get("Source").as_deref(), Some("foo-bar"));
4107        assert_eq!(
4108            para.get("Maintainer").as_deref(),
4109            Some("Foo Bar <test@example.com>")
4110        );
4111    }
4112
4113    #[test]
4114    fn test_normalize_field_spacing_tab_after_colon() {
4115        // Field with tab after colon (should be normalized to single space)
4116        let input = "Field:\tvalue\n";
4117        let deb822 = input.parse::<Deb822>().unwrap();
4118        let mut para = deb822.paragraphs().next().unwrap();
4119
4120        para.normalize_field_spacing();
4121        assert_eq!(para.to_string(), "Field: value\n");
4122    }
4123
4124    #[test]
4125    fn test_set_preserves_indentation() {
4126        // Test that Paragraph.set() preserves the original indentation
4127        let original = r#"Source: example
4128Build-Depends: foo,
4129               bar,
4130               baz
4131"#;
4132
4133        let mut para: super::Paragraph = original.parse().unwrap();
4134
4135        // Modify the Build-Depends field
4136        para.set("Build-Depends", "foo,\nbar,\nbaz");
4137
4138        // The indentation should be preserved (15 spaces for "Build-Depends: ")
4139        let expected = r#"Source: example
4140Build-Depends: foo,
4141               bar,
4142               baz
4143"#;
4144        assert_eq!(para.to_string(), expected);
4145    }
4146
4147    #[test]
4148    fn test_set_new_field_detects_field_name_length_indent() {
4149        // Test that new fields detect field-name-length-based indentation
4150        let original = r#"Source: example
4151Build-Depends: foo,
4152               bar,
4153               baz
4154Depends: lib1,
4155         lib2
4156"#;
4157
4158        let mut para: super::Paragraph = original.parse().unwrap();
4159
4160        // Add a new multi-line field - should detect that indentation is field-name-length + 2
4161        para.set("Recommends", "pkg1,\npkg2,\npkg3");
4162
4163        // "Recommends: " is 12 characters, so indentation should be 12 spaces
4164        assert!(para
4165            .to_string()
4166            .contains("Recommends: pkg1,\n            pkg2,"));
4167    }
4168
4169    #[test]
4170    fn test_set_new_field_detects_fixed_indent() {
4171        // Test that new fields detect fixed indentation pattern
4172        let original = r#"Source: example
4173Build-Depends: foo,
4174     bar,
4175     baz
4176Depends: lib1,
4177     lib2
4178"#;
4179
4180        let mut para: super::Paragraph = original.parse().unwrap();
4181
4182        // Add a new multi-line field - should detect fixed 5-space indentation
4183        para.set("Recommends", "pkg1,\npkg2,\npkg3");
4184
4185        // Should use the same 5-space indentation
4186        assert!(para
4187            .to_string()
4188            .contains("Recommends: pkg1,\n     pkg2,\n     pkg3\n"));
4189    }
4190
4191    #[test]
4192    fn test_set_new_field_no_multiline_fields() {
4193        // Test that new fields use field-name-length when no existing multi-line fields
4194        let original = r#"Source: example
4195Maintainer: Test <test@example.com>
4196"#;
4197
4198        let mut para: super::Paragraph = original.parse().unwrap();
4199
4200        // Add a new multi-line field - should default to field name length + 2
4201        para.set("Depends", "foo,\nbar,\nbaz");
4202
4203        // "Depends: " is 9 characters, so indentation should be 9 spaces
4204        let expected = r#"Source: example
4205Maintainer: Test <test@example.com>
4206Depends: foo,
4207         bar,
4208         baz
4209"#;
4210        assert_eq!(para.to_string(), expected);
4211    }
4212
4213    #[test]
4214    fn test_set_new_field_mixed_indentation() {
4215        // Test that new fields fall back to field-name-length when pattern is inconsistent
4216        let original = r#"Source: example
4217Build-Depends: foo,
4218               bar
4219Depends: lib1,
4220     lib2
4221"#;
4222
4223        let mut para: super::Paragraph = original.parse().unwrap();
4224
4225        // Add a new multi-line field - mixed pattern, should fall back to field name length + 2
4226        para.set("Recommends", "pkg1,\npkg2");
4227
4228        // "Recommends: " is 12 characters
4229        assert!(para
4230            .to_string()
4231            .contains("Recommends: pkg1,\n            pkg2\n"));
4232    }
4233
4234    #[test]
4235    fn test_entry_with_indentation() {
4236        // Test Entry::with_indentation directly
4237        let entry = super::Entry::with_indentation("Test-Field", "value1\nvalue2\nvalue3", "    ");
4238
4239        assert_eq!(
4240            entry.to_string(),
4241            "Test-Field: value1\n    value2\n    value3\n"
4242        );
4243    }
4244
4245    #[test]
4246    fn test_set_with_indent_pattern_fixed() {
4247        // Test setting a field with explicit fixed indentation pattern
4248        let original = r#"Source: example
4249Maintainer: Test <test@example.com>
4250"#;
4251
4252        let mut para: super::Paragraph = original.parse().unwrap();
4253
4254        // Add a new multi-line field with fixed 4-space indentation
4255        para.set_with_indent_pattern(
4256            "Depends",
4257            "foo,\nbar,\nbaz",
4258            Some(&super::IndentPattern::Fixed(4)),
4259            None,
4260        );
4261
4262        // Should use the specified 4-space indentation
4263        let expected = r#"Source: example
4264Maintainer: Test <test@example.com>
4265Depends: foo,
4266    bar,
4267    baz
4268"#;
4269        assert_eq!(para.to_string(), expected);
4270    }
4271
4272    #[test]
4273    fn test_set_with_indent_pattern_field_name_length() {
4274        // Test setting a field with field-name-length indentation pattern
4275        let original = r#"Source: example
4276Maintainer: Test <test@example.com>
4277"#;
4278
4279        let mut para: super::Paragraph = original.parse().unwrap();
4280
4281        // Add a new multi-line field with field-name-length indentation
4282        para.set_with_indent_pattern(
4283            "Build-Depends",
4284            "libfoo,\nlibbar,\nlibbaz",
4285            Some(&super::IndentPattern::FieldNameLength),
4286            None,
4287        );
4288
4289        // "Build-Depends: " is 15 characters, so indentation should be 15 spaces
4290        let expected = r#"Source: example
4291Maintainer: Test <test@example.com>
4292Build-Depends: libfoo,
4293               libbar,
4294               libbaz
4295"#;
4296        assert_eq!(para.to_string(), expected);
4297    }
4298
4299    #[test]
4300    fn test_set_with_indent_pattern_override_auto_detection() {
4301        // Test that explicit default pattern overrides auto-detection for new fields
4302        let original = r#"Source: example
4303Build-Depends: foo,
4304               bar,
4305               baz
4306"#;
4307
4308        let mut para: super::Paragraph = original.parse().unwrap();
4309
4310        // Add a NEW field with fixed 2-space indentation, overriding the auto-detected pattern
4311        para.set_with_indent_pattern(
4312            "Depends",
4313            "lib1,\nlib2,\nlib3",
4314            Some(&super::IndentPattern::Fixed(2)),
4315            None,
4316        );
4317
4318        // Should use the specified 2-space indentation, not the auto-detected 15-space
4319        let expected = r#"Source: example
4320Build-Depends: foo,
4321               bar,
4322               baz
4323Depends: lib1,
4324  lib2,
4325  lib3
4326"#;
4327        assert_eq!(para.to_string(), expected);
4328    }
4329
4330    #[test]
4331    fn test_set_with_indent_pattern_none_auto_detects() {
4332        // Test that None pattern auto-detects from existing fields
4333        let original = r#"Source: example
4334Build-Depends: foo,
4335     bar,
4336     baz
4337"#;
4338
4339        let mut para: super::Paragraph = original.parse().unwrap();
4340
4341        // Add a field with None pattern - should auto-detect fixed 5-space
4342        para.set_with_indent_pattern("Depends", "lib1,\nlib2", None, None);
4343
4344        // Should auto-detect and use the 5-space indentation
4345        let expected = r#"Source: example
4346Build-Depends: foo,
4347     bar,
4348     baz
4349Depends: lib1,
4350     lib2
4351"#;
4352        assert_eq!(para.to_string(), expected);
4353    }
4354
4355    #[test]
4356    fn test_set_with_indent_pattern_with_field_order() {
4357        // Test setting a field with both indent pattern and field ordering
4358        let original = r#"Source: example
4359Maintainer: Test <test@example.com>
4360"#;
4361
4362        let mut para: super::Paragraph = original.parse().unwrap();
4363
4364        // Add a field with fixed indentation and specific field ordering
4365        para.set_with_indent_pattern(
4366            "Priority",
4367            "optional",
4368            Some(&super::IndentPattern::Fixed(4)),
4369            Some(&["Source", "Priority", "Maintainer"]),
4370        );
4371
4372        // Priority should be inserted between Source and Maintainer
4373        let expected = r#"Source: example
4374Priority: optional
4375Maintainer: Test <test@example.com>
4376"#;
4377        assert_eq!(para.to_string(), expected);
4378    }
4379
4380    #[test]
4381    fn test_set_with_indent_pattern_replace_existing() {
4382        // Test that replacing an existing multi-line field preserves its indentation
4383        let original = r#"Source: example
4384Depends: foo,
4385         bar
4386"#;
4387
4388        let mut para: super::Paragraph = original.parse().unwrap();
4389
4390        // Replace Depends - the default pattern is ignored, existing indentation is preserved
4391        para.set_with_indent_pattern(
4392            "Depends",
4393            "lib1,\nlib2,\nlib3",
4394            Some(&super::IndentPattern::Fixed(3)),
4395            None,
4396        );
4397
4398        // Should preserve the existing 9-space indentation, not use the default 3-space
4399        let expected = r#"Source: example
4400Depends: lib1,
4401         lib2,
4402         lib3
4403"#;
4404        assert_eq!(para.to_string(), expected);
4405    }
4406
4407    #[test]
4408    fn test_change_field_indent() {
4409        // Test changing indentation of an existing field without changing its value
4410        let original = r#"Source: example
4411Depends: foo,
4412         bar,
4413         baz
4414"#;
4415        let mut para: super::Paragraph = original.parse().unwrap();
4416
4417        // Change Depends field to use 2-space indentation
4418        let result = para
4419            .change_field_indent("Depends", &super::IndentPattern::Fixed(2))
4420            .unwrap();
4421        assert!(result, "Field should have been found and updated");
4422
4423        let expected = r#"Source: example
4424Depends: foo,
4425  bar,
4426  baz
4427"#;
4428        assert_eq!(para.to_string(), expected);
4429    }
4430
4431    #[test]
4432    fn test_change_field_indent_nonexistent() {
4433        // Test changing indentation of a non-existent field
4434        let original = r#"Source: example
4435"#;
4436        let mut para: super::Paragraph = original.parse().unwrap();
4437
4438        // Try to change indentation of non-existent field
4439        let result = para
4440            .change_field_indent("Depends", &super::IndentPattern::Fixed(2))
4441            .unwrap();
4442        assert!(!result, "Should return false for non-existent field");
4443
4444        // Paragraph should be unchanged
4445        assert_eq!(para.to_string(), original);
4446    }
4447
4448    #[test]
4449    fn test_change_field_indent_case_insensitive() {
4450        // Test that change_field_indent is case-insensitive
4451        let original = r#"Build-Depends: foo,
4452               bar
4453"#;
4454        let mut para: super::Paragraph = original.parse().unwrap();
4455
4456        // Change using different case
4457        let result = para
4458            .change_field_indent("build-depends", &super::IndentPattern::Fixed(1))
4459            .unwrap();
4460        assert!(result, "Should find field case-insensitively");
4461
4462        let expected = r#"Build-Depends: foo,
4463 bar
4464"#;
4465        assert_eq!(para.to_string(), expected);
4466    }
4467
4468    #[test]
4469    fn test_entry_get_indent() {
4470        // Test that we can extract indentation from an entry
4471        let original = r#"Build-Depends: foo,
4472               bar,
4473               baz
4474"#;
4475        let para: super::Paragraph = original.parse().unwrap();
4476        let entry = para.entries().next().unwrap();
4477
4478        assert_eq!(entry.get_indent(), Some("               ".to_string()));
4479    }
4480
4481    #[test]
4482    fn test_entry_get_indent_single_line() {
4483        // Single-line entries should return None for indentation
4484        let original = r#"Source: example
4485"#;
4486        let para: super::Paragraph = original.parse().unwrap();
4487        let entry = para.entries().next().unwrap();
4488
4489        assert_eq!(entry.get_indent(), None);
4490    }
4491}
4492
4493#[test]
4494fn test_move_paragraph_forward() {
4495    let mut d: Deb822 = vec![
4496        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4497        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4498        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4499    ]
4500    .into_iter()
4501    .collect();
4502    d.move_paragraph(0, 2);
4503    assert_eq!(
4504        d.to_string(),
4505        "A: B\nC: D\n\nX: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n"
4506    );
4507}
4508
4509#[test]
4510fn test_move_paragraph_backward() {
4511    let mut d: Deb822 = vec![
4512        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4513        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4514        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4515    ]
4516    .into_iter()
4517    .collect();
4518    d.move_paragraph(2, 0);
4519    assert_eq!(
4520        d.to_string(),
4521        "X: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n\nA: B\nC: D\n"
4522    );
4523}
4524
4525#[test]
4526fn test_move_paragraph_middle() {
4527    let mut d: Deb822 = vec![
4528        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4529        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4530        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4531    ]
4532    .into_iter()
4533    .collect();
4534    d.move_paragraph(2, 1);
4535    assert_eq!(
4536        d.to_string(),
4537        "Foo: Bar\nBaz: Qux\n\nX: Y\nZ: W\n\nA: B\nC: D\n"
4538    );
4539}
4540
4541#[test]
4542fn test_move_paragraph_same_index() {
4543    let mut d: Deb822 = vec![
4544        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4545        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4546    ]
4547    .into_iter()
4548    .collect();
4549    let original = d.to_string();
4550    d.move_paragraph(1, 1);
4551    assert_eq!(d.to_string(), original);
4552}
4553
4554#[test]
4555fn test_move_paragraph_single() {
4556    let mut d: Deb822 = vec![vec![("Foo", "Bar")].into_iter().collect()]
4557        .into_iter()
4558        .collect();
4559    let original = d.to_string();
4560    d.move_paragraph(0, 0);
4561    assert_eq!(d.to_string(), original);
4562}
4563
4564#[test]
4565fn test_move_paragraph_invalid_index() {
4566    let mut d: Deb822 = vec![
4567        vec![("Foo", "Bar")].into_iter().collect(),
4568        vec![("A", "B")].into_iter().collect(),
4569    ]
4570    .into_iter()
4571    .collect();
4572    let original = d.to_string();
4573    d.move_paragraph(0, 5);
4574    assert_eq!(d.to_string(), original);
4575}
4576
4577#[test]
4578fn test_move_paragraph_with_comments() {
4579    let text = r#"Foo: Bar
4580
4581# This is a comment
4582
4583A: B
4584
4585X: Y
4586"#;
4587    let mut d: Deb822 = text.parse().unwrap();
4588    d.move_paragraph(0, 2);
4589    assert_eq!(
4590        d.to_string(),
4591        "# This is a comment\n\nA: B\n\nX: Y\n\nFoo: Bar\n"
4592    );
4593}
4594
4595#[test]
4596fn test_case_insensitive_get() {
4597    let text = "Package: test\nVersion: 1.0\n";
4598    let d: Deb822 = text.parse().unwrap();
4599    let p = d.paragraphs().next().unwrap();
4600
4601    // Test different case variations
4602    assert_eq!(p.get("Package").as_deref(), Some("test"));
4603    assert_eq!(p.get("package").as_deref(), Some("test"));
4604    assert_eq!(p.get("PACKAGE").as_deref(), Some("test"));
4605    assert_eq!(p.get("PaCkAgE").as_deref(), Some("test"));
4606
4607    assert_eq!(p.get("Version").as_deref(), Some("1.0"));
4608    assert_eq!(p.get("version").as_deref(), Some("1.0"));
4609    assert_eq!(p.get("VERSION").as_deref(), Some("1.0"));
4610}
4611
4612#[test]
4613fn test_case_insensitive_set() {
4614    let text = "Package: test\n";
4615    let d: Deb822 = text.parse().unwrap();
4616    let mut p = d.paragraphs().next().unwrap();
4617
4618    // Set with different case should update the existing field
4619    p.set("package", "updated");
4620    assert_eq!(p.get("Package").as_deref(), Some("updated"));
4621    assert_eq!(p.get("package").as_deref(), Some("updated"));
4622
4623    // Set with UPPERCASE
4624    p.set("PACKAGE", "updated2");
4625    assert_eq!(p.get("Package").as_deref(), Some("updated2"));
4626
4627    // Field count should remain 1
4628    assert_eq!(p.keys().count(), 1);
4629}
4630
4631#[test]
4632fn test_case_insensitive_remove() {
4633    let text = "Package: test\nVersion: 1.0\n";
4634    let d: Deb822 = text.parse().unwrap();
4635    let mut p = d.paragraphs().next().unwrap();
4636
4637    // Remove with different case
4638    p.remove("package");
4639    assert_eq!(p.get("Package"), None);
4640    assert_eq!(p.get("Version").as_deref(), Some("1.0"));
4641
4642    // Remove with uppercase
4643    p.remove("VERSION");
4644    assert_eq!(p.get("Version"), None);
4645
4646    // No fields left
4647    assert_eq!(p.keys().count(), 0);
4648}
4649
4650#[test]
4651fn test_case_preservation() {
4652    let text = "Package: test\n";
4653    let d: Deb822 = text.parse().unwrap();
4654    let mut p = d.paragraphs().next().unwrap();
4655
4656    // Original case should be preserved
4657    let original_text = d.to_string();
4658    assert_eq!(original_text, "Package: test\n");
4659
4660    // Set with different case should preserve original case
4661    p.set("package", "updated");
4662
4663    // The field name should still be "Package" (original case preserved)
4664    let updated_text = d.to_string();
4665    assert_eq!(updated_text, "Package: updated\n");
4666}
4667
4668#[test]
4669fn test_case_insensitive_contains_key() {
4670    let text = "Package: test\n";
4671    let d: Deb822 = text.parse().unwrap();
4672    let p = d.paragraphs().next().unwrap();
4673
4674    assert!(p.contains_key("Package"));
4675    assert!(p.contains_key("package"));
4676    assert!(p.contains_key("PACKAGE"));
4677    assert!(!p.contains_key("NonExistent"));
4678}
4679
4680#[test]
4681fn test_case_insensitive_get_all() {
4682    let text = "Package: test1\npackage: test2\n";
4683    let d: Deb822 = text.parse().unwrap();
4684    let p = d.paragraphs().next().unwrap();
4685
4686    let values: Vec<String> = p.get_all("PACKAGE").collect();
4687    assert_eq!(values, vec!["test1", "test2"]);
4688}
4689
4690#[test]
4691fn test_case_insensitive_rename() {
4692    let text = "Package: test\n";
4693    let d: Deb822 = text.parse().unwrap();
4694    let mut p = d.paragraphs().next().unwrap();
4695
4696    // Rename with different case
4697    assert!(p.rename("package", "NewName"));
4698    assert_eq!(p.get("NewName").as_deref(), Some("test"));
4699    assert_eq!(p.get("Package"), None);
4700}
4701
4702#[test]
4703fn test_rename_changes_case() {
4704    let text = "Package: test\n";
4705    let d: Deb822 = text.parse().unwrap();
4706    let mut p = d.paragraphs().next().unwrap();
4707
4708    // Rename to different case of the same name
4709    assert!(p.rename("package", "PACKAGE"));
4710
4711    // The field name should now be uppercase
4712    let updated_text = d.to_string();
4713    assert_eq!(updated_text, "PACKAGE: test\n");
4714
4715    // Can still get with any case
4716    assert_eq!(p.get("package").as_deref(), Some("test"));
4717    assert_eq!(p.get("Package").as_deref(), Some("test"));
4718    assert_eq!(p.get("PACKAGE").as_deref(), Some("test"));
4719}
4720
4721#[test]
4722fn test_reject_whitespace_only_continuation_line() {
4723    // Issue #350: A continuation line with only whitespace should not be accepted
4724    // According to Debian Policy, continuation lines must have content after the leading space
4725    // A line with only whitespace (like " \n") should terminate the field
4726
4727    // This should be rejected/treated as an error
4728    let text = "Build-Depends:\n \ndebhelper\n";
4729    let parsed = Deb822::parse(text);
4730
4731    // The empty line with just whitespace should cause an error
4732    // or at minimum, should not be included as part of the field value
4733    assert!(
4734        !parsed.errors().is_empty(),
4735        "Expected parse errors for whitespace-only continuation line"
4736    );
4737}
4738
4739#[test]
4740fn test_reject_empty_continuation_line_in_multiline_field() {
4741    // Test that an empty line terminates a multi-line field (and generates an error)
4742    let text = "Depends: foo,\n bar,\n \n baz\n";
4743    let parsed = Deb822::parse(text);
4744
4745    // The empty line should cause parse errors
4746    assert!(
4747        !parsed.errors().is_empty(),
4748        "Empty continuation line should generate parse errors"
4749    );
4750
4751    // Verify we got the specific error about empty continuation line
4752    let has_empty_line_error = parsed
4753        .errors()
4754        .iter()
4755        .any(|e| e.contains("empty continuation line"));
4756    assert!(
4757        has_empty_line_error,
4758        "Should have an error about empty continuation line"
4759    );
4760}
4761
4762#[test]
4763#[should_panic(expected = "empty continuation line")]
4764fn test_set_rejects_empty_continuation_lines() {
4765    // Test that Paragraph.set() panics for values with empty continuation lines
4766    let text = "Package: test\n";
4767    let deb822 = text.parse::<Deb822>().unwrap();
4768    let mut para = deb822.paragraphs().next().unwrap();
4769
4770    // Try to set a field with an empty continuation line
4771    // This should panic with an appropriate error message
4772    let value_with_empty_line = "foo\n \nbar";
4773    para.set("Depends", value_with_empty_line);
4774}
4775
4776#[test]
4777fn test_try_set_returns_error_for_empty_continuation_lines() {
4778    // Test that Paragraph.try_set() returns an error for values with empty continuation lines
4779    let text = "Package: test\n";
4780    let deb822 = text.parse::<Deb822>().unwrap();
4781    let mut para = deb822.paragraphs().next().unwrap();
4782
4783    // Try to set a field with an empty continuation line
4784    let value_with_empty_line = "foo\n \nbar";
4785    let result = para.try_set("Depends", value_with_empty_line);
4786
4787    // Should return an error
4788    assert!(
4789        result.is_err(),
4790        "try_set() should return an error for empty continuation lines"
4791    );
4792
4793    // Verify it's the right kind of error
4794    match result {
4795        Err(Error::InvalidValue(msg)) => {
4796            assert!(
4797                msg.contains("empty continuation line"),
4798                "Error message should mention empty continuation line"
4799            );
4800        }
4801        _ => panic!("Expected InvalidValue error"),
4802    }
4803}
4804
4805#[test]
4806fn test_try_set_with_indent_pattern_returns_error() {
4807    // Test that try_set_with_indent_pattern() returns an error for empty continuation lines
4808    let text = "Package: test\n";
4809    let deb822 = text.parse::<Deb822>().unwrap();
4810    let mut para = deb822.paragraphs().next().unwrap();
4811
4812    let value_with_empty_line = "foo\n \nbar";
4813    let result = para.try_set_with_indent_pattern(
4814        "Depends",
4815        value_with_empty_line,
4816        Some(&IndentPattern::Fixed(2)),
4817        None,
4818    );
4819
4820    assert!(
4821        result.is_err(),
4822        "try_set_with_indent_pattern() should return an error"
4823    );
4824}
4825
4826#[test]
4827fn test_try_set_succeeds_for_valid_value() {
4828    // Test that try_set() succeeds for valid values
4829    let text = "Package: test\n";
4830    let deb822 = text.parse::<Deb822>().unwrap();
4831    let mut para = deb822.paragraphs().next().unwrap();
4832
4833    // Valid multiline value
4834    let valid_value = "foo\nbar";
4835    let result = para.try_set("Depends", valid_value);
4836
4837    assert!(result.is_ok(), "try_set() should succeed for valid values");
4838    assert_eq!(para.get("Depends").as_deref(), Some("foo\nbar"));
4839}
4840
4841#[test]
4842fn test_field_with_empty_first_line() {
4843    // Test parsing a field where the value starts on a continuation line (empty first line)
4844    // This is valid according to Debian Policy - the first line can be empty
4845    let text = "Foo:\n blah\n blah\n";
4846    let parsed = Deb822::parse(text);
4847
4848    // This should be valid - no errors
4849    assert!(
4850        parsed.errors().is_empty(),
4851        "Empty first line should be valid. Got errors: {:?}",
4852        parsed.errors()
4853    );
4854
4855    let deb822 = parsed.tree();
4856    let para = deb822.paragraphs().next().unwrap();
4857    assert_eq!(para.get("Foo").as_deref(), Some("blah\nblah"));
4858}
4859
4860#[test]
4861fn test_try_set_with_empty_first_line() {
4862    // Test that try_set() works with values that have empty first line
4863    let text = "Package: test\n";
4864    let deb822 = text.parse::<Deb822>().unwrap();
4865    let mut para = deb822.paragraphs().next().unwrap();
4866
4867    // Value with empty first line - this should be valid
4868    let value = "\nblah\nmore";
4869    let result = para.try_set("Depends", value);
4870
4871    assert!(
4872        result.is_ok(),
4873        "try_set() should succeed for values with empty first line. Got: {:?}",
4874        result
4875    );
4876}
4877
4878#[test]
4879fn test_field_with_value_then_empty_continuation() {
4880    // Test that a field with a value on the first line followed by empty continuation is rejected
4881    let text = "Foo: bar\n \n";
4882    let parsed = Deb822::parse(text);
4883
4884    // This should have errors - empty continuation line after initial value
4885    assert!(
4886        !parsed.errors().is_empty(),
4887        "Field with value then empty continuation line should be rejected"
4888    );
4889
4890    // Verify we got the specific error about empty continuation line
4891    let has_empty_line_error = parsed
4892        .errors()
4893        .iter()
4894        .any(|e| e.contains("empty continuation line"));
4895    assert!(
4896        has_empty_line_error,
4897        "Should have error about empty continuation line"
4898    );
4899}
4900
4901#[test]
4902fn test_line_col() {
4903    let text = r#"Source: foo
4904Maintainer: Foo Bar <jelmer@jelmer.uk>
4905Section: net
4906
4907Package: foo
4908Architecture: all
4909Depends: libc6
4910Description: This is a description
4911 With details
4912"#;
4913    let deb822 = text.parse::<Deb822>().unwrap();
4914
4915    // Test paragraph line numbers
4916    let paras: Vec<_> = deb822.paragraphs().collect();
4917    assert_eq!(paras.len(), 2);
4918
4919    // First paragraph starts at line 0
4920    assert_eq!(paras[0].line(), 0);
4921    assert_eq!(paras[0].column(), 0);
4922
4923    // Second paragraph starts at line 4 (after the empty line)
4924    assert_eq!(paras[1].line(), 4);
4925    assert_eq!(paras[1].column(), 0);
4926
4927    // Test entry line numbers
4928    let entries: Vec<_> = paras[0].entries().collect();
4929    assert_eq!(entries[0].line(), 0); // Source: foo
4930    assert_eq!(entries[1].line(), 1); // Maintainer: ...
4931    assert_eq!(entries[2].line(), 2); // Section: net
4932
4933    // Test column numbers
4934    assert_eq!(entries[0].column(), 0); // Start of line
4935    assert_eq!(entries[1].column(), 0); // Start of line
4936
4937    // Test line_col() method
4938    assert_eq!(paras[1].line_col(), (4, 0));
4939    assert_eq!(entries[0].line_col(), (0, 0));
4940
4941    // Test multi-line entry
4942    let second_para_entries: Vec<_> = paras[1].entries().collect();
4943    assert_eq!(second_para_entries[3].line(), 7); // Description starts at line 7
4944}
4945
4946#[test]
4947fn test_deb822_snapshot_independence() {
4948    // Test that snapshot() creates an independent copy
4949    let text = r#"Source: foo
4950Maintainer: Joe <joe@example.com>
4951
4952Package: foo
4953Architecture: all
4954"#;
4955    let deb822 = text.parse::<Deb822>().unwrap();
4956    let snapshot = deb822.snapshot();
4957
4958    // Modify the original
4959    let mut para = deb822.paragraphs().next().unwrap();
4960    para.set("Source", "modified");
4961
4962    // Verify the snapshot is unchanged
4963    let snapshot_para = snapshot.paragraphs().next().unwrap();
4964    assert_eq!(snapshot_para.get("Source").as_deref(), Some("foo"));
4965
4966    // Modify the snapshot
4967    let mut snapshot_para = snapshot.paragraphs().next().unwrap();
4968    snapshot_para.set("Source", "snapshot-modified");
4969
4970    // Verify the original still has our first modification
4971    let para = deb822.paragraphs().next().unwrap();
4972    assert_eq!(para.get("Source").as_deref(), Some("modified"));
4973}
4974
4975#[test]
4976fn test_paragraph_snapshot_independence() {
4977    // Test that snapshot() creates an independent copy
4978    let text = "Package: foo\nArchitecture: all\n";
4979    let deb822 = text.parse::<Deb822>().unwrap();
4980    let mut para = deb822.paragraphs().next().unwrap();
4981    let mut snapshot = para.snapshot();
4982
4983    // Modify the original
4984    para.set("Package", "modified");
4985
4986    // Verify the snapshot is unchanged
4987    assert_eq!(snapshot.get("Package").as_deref(), Some("foo"));
4988
4989    // Modify the snapshot
4990    snapshot.set("Package", "snapshot-modified");
4991
4992    // Verify the original still has our first modification
4993    assert_eq!(para.get("Package").as_deref(), Some("modified"));
4994}
4995
4996#[test]
4997fn test_entry_snapshot_independence() {
4998    // Test that snapshot() creates an independent copy
4999    let text = "Package: foo\n";
5000    let deb822 = text.parse::<Deb822>().unwrap();
5001    let mut para = deb822.paragraphs().next().unwrap();
5002    let entry = para.entries().next().unwrap();
5003    let snapshot = entry.snapshot();
5004
5005    // Get values before modification
5006    let original_value = entry.value();
5007    let snapshot_value = snapshot.value();
5008
5009    // Both should start with the same value
5010    assert_eq!(original_value, "foo");
5011    assert_eq!(snapshot_value, "foo");
5012
5013    // Modify through the paragraph
5014    para.set("Package", "modified");
5015
5016    // Verify the entry reflects the change (since it points to the same paragraph)
5017    let entry_after = para.entries().next().unwrap();
5018    assert_eq!(entry_after.value(), "modified");
5019
5020    // But the snapshot entry should still have the original value
5021    // (it points to a different tree)
5022    assert_eq!(snapshot.value(), "foo");
5023}
5024
5025#[test]
5026fn test_snapshot_preserves_structure() {
5027    // Test that snapshot() preserves comments, whitespace, etc.
5028    let text = r#"# Comment
5029Source: foo
5030## Another comment
5031Maintainer: Joe <joe@example.com>
5032
5033Package: foo
5034Architecture: all
5035"#;
5036    let deb822 = text.parse::<Deb822>().unwrap();
5037    let snapshot = deb822.snapshot();
5038
5039    // Both should have the same structure
5040    assert_eq!(deb822.to_string(), snapshot.to_string());
5041
5042    // Verify they're independent
5043    let mut snapshot_para = snapshot.paragraphs().next().unwrap();
5044    snapshot_para.set("Source", "modified");
5045
5046    let original_para = deb822.paragraphs().next().unwrap();
5047    assert_eq!(original_para.get("Source").as_deref(), Some("foo"));
5048}