Skip to main content

deb822_lossless/
lossless.rs

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