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