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