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    /// Returns the text range of the first whitespace-delimited token of the
2522    /// value.
2523    ///
2524    /// For single-word values (e.g. `Source: hello`) this is the whole value.
2525    /// For multi-word values it covers just the first word, which is useful for
2526    /// fields whose first token is an identifier — for example the license
2527    /// short-name in a DEP-5 `License:` field (`GPL-2+ with exceptions`).
2528    /// Returns `None` if the value is empty.
2529    pub fn value_token_range(&self) -> Option<rowan::TextRange> {
2530        let value_range = self.value_range()?;
2531        let first = self
2532            .0
2533            .children_with_tokens()
2534            .filter_map(|it| it.into_token())
2535            .find(|it| it.kind() == VALUE)?;
2536        let text = first.text();
2537        let leading_ws = text.len() - text.trim_start().len();
2538        let token_len = text[leading_ws..]
2539            .find(char::is_whitespace)
2540            .unwrap_or(text.len() - leading_ws);
2541        if token_len == 0 {
2542            return None;
2543        }
2544        let start = first.text_range().start() + rowan::TextSize::from(leading_ws as u32);
2545        let end = start + rowan::TextSize::from(token_len as u32);
2546        // Defensively clamp to the value range.
2547        if start < value_range.start() || end > value_range.end() {
2548            return None;
2549        }
2550        Some(rowan::TextRange::new(start, end))
2551    }
2552
2553    /// Create a new entry with the given key and value.
2554    pub fn new(key: &str, value: &str) -> Entry {
2555        Self::with_indentation(key, value, " ")
2556    }
2557
2558    /// Create a new entry with the given key, value, and custom indentation for continuation lines.
2559    ///
2560    /// # Arguments
2561    /// * `key` - The field name
2562    /// * `value` - The field value (may contain '\n' for multi-line values)
2563    /// * `indent` - The indentation string to use for continuation lines
2564    pub fn with_indentation(key: &str, value: &str, indent: &str) -> Entry {
2565        Entry::with_formatting(key, value, " ", indent)
2566    }
2567
2568    /// Try to create a new entry with specific formatting, validating the value.
2569    ///
2570    /// # Arguments
2571    /// * `key` - The field name
2572    /// * `value` - The field value (may contain '\n' for multi-line values)
2573    /// * `post_colon_ws` - The whitespace after the colon (e.g., " " or "\n ")
2574    /// * `indent` - The indentation string to use for continuation lines
2575    ///
2576    /// # Errors
2577    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
2578    pub fn try_with_formatting(
2579        key: &str,
2580        value: &str,
2581        post_colon_ws: &str,
2582        indent: &str,
2583    ) -> Result<Entry, Error> {
2584        let mut builder = GreenNodeBuilder::new();
2585
2586        builder.start_node(ENTRY.into());
2587        builder.token(KEY.into(), key);
2588        builder.token(COLON.into(), ":");
2589
2590        // Add the post-colon whitespace token by token
2591        let mut i = 0;
2592        while i < post_colon_ws.len() {
2593            if post_colon_ws[i..].starts_with('\n') {
2594                builder.token(NEWLINE.into(), "\n");
2595                i += 1;
2596            } else {
2597                // Collect consecutive non-newline chars as WHITESPACE
2598                let start = i;
2599                while i < post_colon_ws.len() && !post_colon_ws[i..].starts_with('\n') {
2600                    i += post_colon_ws[i..].chars().next().unwrap().len_utf8();
2601                }
2602                builder.token(WHITESPACE.into(), &post_colon_ws[start..i]);
2603            }
2604        }
2605
2606        for (line_idx, line) in value.split('\n').enumerate() {
2607            if line_idx > 0 {
2608                // Validate that continuation lines are not empty or whitespace-only
2609                // According to Debian Policy, continuation lines must have content
2610                if line.trim().is_empty() {
2611                    return Err(Error::InvalidValue(format!(
2612                        "empty continuation line (line with only whitespace) at line {}",
2613                        line_idx + 1
2614                    )));
2615                }
2616                builder.token(INDENT.into(), indent);
2617            }
2618            builder.token(VALUE.into(), line);
2619            builder.token(NEWLINE.into(), "\n");
2620        }
2621        builder.finish_node();
2622        Ok(Entry(SyntaxNode::new_root_mut(builder.finish())))
2623    }
2624
2625    /// Create a new entry with specific formatting for post-colon whitespace and indentation.
2626    ///
2627    /// # Arguments
2628    /// * `key` - The field name
2629    /// * `value` - The field value (may contain '\n' for multi-line values)
2630    /// * `post_colon_ws` - The whitespace after the colon (e.g., " " or "\n ")
2631    /// * `indent` - The indentation string to use for continuation lines
2632    ///
2633    /// # Panics
2634    /// Panics if the value contains empty continuation lines (lines with only whitespace)
2635    pub fn with_formatting(key: &str, value: &str, post_colon_ws: &str, indent: &str) -> Entry {
2636        Self::try_with_formatting(key, value, post_colon_ws, indent)
2637            .expect("Invalid value: empty continuation line")
2638    }
2639
2640    #[must_use]
2641    /// Reformat this entry
2642    ///
2643    /// # Arguments
2644    /// * `indentation` - The indentation to use
2645    /// * `immediate_empty_line` - Whether multi-line values should always start with an empty line
2646    /// * `max_line_length_one_liner` - If set, then this is the max length of the value if it is
2647    ///   crammed into a "one-liner" value
2648    /// * `format_value` - If set, then this function will format the value according to the given
2649    ///   function
2650    ///
2651    /// # Returns
2652    /// The reformatted entry
2653    pub fn wrap_and_sort(
2654        &self,
2655        mut indentation: Indentation,
2656        immediate_empty_line: bool,
2657        max_line_length_one_liner: Option<usize>,
2658        format_value: Option<&dyn Fn(&str, &str) -> String>,
2659    ) -> Entry {
2660        let mut builder = GreenNodeBuilder::new();
2661
2662        let mut content = vec![];
2663        builder.start_node(ENTRY.into());
2664        for c in self.0.children_with_tokens() {
2665            let text = c.as_token().map(|t| t.text());
2666            match c.kind() {
2667                KEY => {
2668                    builder.token(KEY.into(), text.unwrap());
2669                    if indentation == Indentation::FieldNameLength {
2670                        indentation = Indentation::Spaces(text.unwrap().len() as u32);
2671                    }
2672                }
2673                COLON => {
2674                    builder.token(COLON.into(), ":");
2675                }
2676                INDENT => {
2677                    // Discard original whitespace
2678                }
2679                ERROR | COMMENT | VALUE | WHITESPACE | NEWLINE => {
2680                    content.push(c);
2681                }
2682                EMPTY_LINE | ENTRY | ROOT | PARAGRAPH => unreachable!(),
2683            }
2684        }
2685
2686        let indentation = if let crate::Indentation::Spaces(i) = indentation {
2687            i
2688        } else {
2689            1
2690        };
2691
2692        assert!(indentation > 0);
2693
2694        // Strip trailing whitespace and newlines
2695        while let Some(c) = content.last() {
2696            if c.kind() == NEWLINE || c.kind() == WHITESPACE {
2697                content.pop();
2698            } else {
2699                break;
2700            }
2701        }
2702
2703        // Reformat iff there is a format function and the value
2704        // has no errors or comments
2705        let tokens = if let Some(ref format_value) = format_value {
2706            if !content
2707                .iter()
2708                .any(|c| c.kind() == ERROR || c.kind() == COMMENT)
2709            {
2710                let concat = content
2711                    .iter()
2712                    .filter_map(|c| c.as_token().map(|t| t.text()))
2713                    .collect::<String>();
2714                let formatted = format_value(self.key().as_ref().unwrap(), &concat);
2715                crate::lex::lex_inline(&formatted)
2716                    .map(|(k, t)| (k, t.to_string()))
2717                    .collect::<Vec<_>>()
2718            } else {
2719                content
2720                    .into_iter()
2721                    .map(|n| n.into_token().unwrap())
2722                    .map(|i| (i.kind(), i.text().to_string()))
2723                    .collect::<Vec<_>>()
2724            }
2725        } else {
2726            content
2727                .into_iter()
2728                .map(|n| n.into_token().unwrap())
2729                .map(|i| (i.kind(), i.text().to_string()))
2730                .collect::<Vec<_>>()
2731        };
2732
2733        rebuild_value(
2734            &mut builder,
2735            tokens,
2736            self.key().map_or(0, |k| k.len()),
2737            indentation,
2738            immediate_empty_line,
2739            max_line_length_one_liner,
2740        );
2741
2742        builder.finish_node();
2743        Self(SyntaxNode::new_root_mut(builder.finish()))
2744    }
2745
2746    /// Returns the key of the entry.
2747    pub fn key(&self) -> Option<String> {
2748        self.0
2749            .children_with_tokens()
2750            .filter_map(|it| it.into_token())
2751            .find(|it| it.kind() == KEY)
2752            .map(|it| it.text().to_string())
2753    }
2754
2755    /// Returns the value of the entry.
2756    pub fn value(&self) -> String {
2757        let mut parts = self
2758            .0
2759            .children_with_tokens()
2760            .filter_map(|it| it.into_token())
2761            .filter(|it| it.kind() == VALUE)
2762            .map(|it| it.text().to_string());
2763
2764        match parts.next() {
2765            None => String::new(),
2766            Some(first) => {
2767                let mut result = first;
2768                for part in parts {
2769                    result.push('\n');
2770                    result.push_str(&part);
2771                }
2772                result
2773            }
2774        }
2775    }
2776
2777    /// Returns the value of this entry, including any comment lines embedded
2778    /// within the multi-line value.
2779    ///
2780    /// This is like [`value()`](Self::value) but also includes `#`-prefixed
2781    /// comment lines that appear between continuation lines. This is useful
2782    /// for parsers (e.g. Relations) that need to preserve commented-out entries.
2783    pub fn value_with_comments(&self) -> String {
2784        let mut parts = self
2785            .0
2786            .children_with_tokens()
2787            .filter_map(|it| it.into_token())
2788            .filter(|it| it.kind() == VALUE || it.kind() == COMMENT)
2789            .map(|it| it.text().to_string());
2790
2791        match parts.next() {
2792            None => String::new(),
2793            Some(first) => {
2794                let mut result = first;
2795                for part in parts {
2796                    result.push('\n');
2797                    result.push_str(&part);
2798                }
2799                result
2800            }
2801        }
2802    }
2803
2804    /// Returns the indentation string used for continuation lines in this entry.
2805    /// Returns None if the entry has no continuation lines.
2806    fn get_indent(&self) -> Option<String> {
2807        self.0
2808            .children_with_tokens()
2809            .filter_map(|it| it.into_token())
2810            .find(|it| it.kind() == INDENT)
2811            .map(|it| it.text().to_string())
2812    }
2813
2814    /// Returns the whitespace immediately after the colon in this entry.
2815    /// This includes WHITESPACE, NEWLINE, and INDENT tokens up to the first VALUE token.
2816    /// Returns None if there is no whitespace (which would be malformed).
2817    fn get_post_colon_whitespace(&self) -> Option<String> {
2818        let mut found_colon = false;
2819        let mut whitespace = String::new();
2820
2821        for token in self
2822            .0
2823            .children_with_tokens()
2824            .filter_map(|it| it.into_token())
2825        {
2826            if token.kind() == COLON {
2827                found_colon = true;
2828                continue;
2829            }
2830
2831            if found_colon {
2832                if token.kind() == WHITESPACE || token.kind() == NEWLINE || token.kind() == INDENT {
2833                    whitespace.push_str(token.text());
2834                } else {
2835                    // We've reached a non-whitespace token, stop collecting
2836                    break;
2837                }
2838            }
2839        }
2840
2841        if whitespace.is_empty() {
2842            None
2843        } else {
2844            Some(whitespace)
2845        }
2846    }
2847
2848    /// Normalize the spacing around the field separator (colon) in place.
2849    ///
2850    /// This ensures that there is exactly one space after the colon and before the value.
2851    /// This is a lossless operation that preserves the field name and value content,
2852    /// but normalizes the whitespace formatting.
2853    ///
2854    /// # Examples
2855    ///
2856    /// ```
2857    /// use deb822_lossless::Deb822;
2858    /// use std::str::FromStr;
2859    ///
2860    /// // Parse an entry with extra spacing after the colon
2861    /// let input = "Field:    value\n";
2862    /// let mut deb822 = Deb822::from_str(input).unwrap();
2863    /// let mut para = deb822.paragraphs().next().unwrap();
2864    ///
2865    /// para.normalize_field_spacing();
2866    /// assert_eq!(para.get("Field").as_deref(), Some("value"));
2867    /// ```
2868    pub fn normalize_field_spacing(&mut self) -> bool {
2869        use rowan::GreenNodeBuilder;
2870
2871        // Store the original text for comparison
2872        let original_text = self.0.text().to_string();
2873
2874        // Build normalized entry
2875        let mut builder = GreenNodeBuilder::new();
2876        builder.start_node(ENTRY.into());
2877
2878        let mut seen_colon = false;
2879        let mut skip_whitespace = false;
2880
2881        for child in self.0.children_with_tokens() {
2882            match child.kind() {
2883                KEY => {
2884                    builder.token(KEY.into(), child.as_token().unwrap().text());
2885                }
2886                COLON => {
2887                    builder.token(COLON.into(), ":");
2888                    seen_colon = true;
2889                    skip_whitespace = true;
2890                }
2891                WHITESPACE if skip_whitespace => {
2892                    // Skip existing whitespace after colon
2893                    continue;
2894                }
2895                VALUE if skip_whitespace => {
2896                    // Add exactly one space before the first value token
2897                    builder.token(WHITESPACE.into(), " ");
2898                    builder.token(VALUE.into(), child.as_token().unwrap().text());
2899                    skip_whitespace = false;
2900                }
2901                NEWLINE if skip_whitespace && seen_colon => {
2902                    // Empty value case (e.g., "Field:\n" or "Field:  \n")
2903                    // Normalize to no trailing space - just output newline
2904                    builder.token(NEWLINE.into(), "\n");
2905                    skip_whitespace = false;
2906                }
2907                _ => {
2908                    // Copy all other tokens as-is
2909                    if let Some(token) = child.as_token() {
2910                        builder.token(token.kind().into(), token.text());
2911                    }
2912                }
2913            }
2914        }
2915
2916        builder.finish_node();
2917        let normalized_green = builder.finish();
2918        let normalized = SyntaxNode::new_root_mut(normalized_green);
2919
2920        // Check if normalization made any changes
2921        let changed = original_text != normalized.text().to_string();
2922
2923        if changed {
2924            // Replace this entry in place
2925            if let Some(parent) = self.0.parent() {
2926                let index = self.0.index();
2927                parent.splice_children(index..index + 1, vec![normalized.into()]);
2928            }
2929        }
2930
2931        changed
2932    }
2933
2934    /// Detach this entry from the paragraph.
2935    pub fn detach(&mut self) {
2936        self.0.detach();
2937    }
2938}
2939
2940impl FromStr for Deb822 {
2941    type Err = ParseError;
2942
2943    fn from_str(s: &str) -> Result<Self, Self::Err> {
2944        Deb822::parse(s).to_result()
2945    }
2946}
2947
2948#[test]
2949fn test_parse_simple() {
2950    const CONTROLV1: &str = r#"Source: foo
2951Maintainer: Foo Bar <foo@example.com>
2952Section: net
2953
2954# This is a comment
2955
2956Package: foo
2957Architecture: all
2958Depends:
2959 bar,
2960 blah
2961Description: This is a description
2962 And it is
2963 .
2964 multiple
2965 lines
2966"#;
2967    let parsed = parse(CONTROLV1);
2968    let node = parsed.syntax();
2969    assert_eq!(
2970        format!("{:#?}", node),
2971        r###"ROOT@0..203
2972  PARAGRAPH@0..63
2973    ENTRY@0..12
2974      KEY@0..6 "Source"
2975      COLON@6..7 ":"
2976      WHITESPACE@7..8 " "
2977      VALUE@8..11 "foo"
2978      NEWLINE@11..12 "\n"
2979    ENTRY@12..50
2980      KEY@12..22 "Maintainer"
2981      COLON@22..23 ":"
2982      WHITESPACE@23..24 " "
2983      VALUE@24..49 "Foo Bar <foo@example. ..."
2984      NEWLINE@49..50 "\n"
2985    ENTRY@50..63
2986      KEY@50..57 "Section"
2987      COLON@57..58 ":"
2988      WHITESPACE@58..59 " "
2989      VALUE@59..62 "net"
2990      NEWLINE@62..63 "\n"
2991  EMPTY_LINE@63..64
2992    NEWLINE@63..64 "\n"
2993  EMPTY_LINE@64..84
2994    COMMENT@64..83 "# This is a comment"
2995    NEWLINE@83..84 "\n"
2996  EMPTY_LINE@84..85
2997    NEWLINE@84..85 "\n"
2998  PARAGRAPH@85..203
2999    ENTRY@85..98
3000      KEY@85..92 "Package"
3001      COLON@92..93 ":"
3002      WHITESPACE@93..94 " "
3003      VALUE@94..97 "foo"
3004      NEWLINE@97..98 "\n"
3005    ENTRY@98..116
3006      KEY@98..110 "Architecture"
3007      COLON@110..111 ":"
3008      WHITESPACE@111..112 " "
3009      VALUE@112..115 "all"
3010      NEWLINE@115..116 "\n"
3011    ENTRY@116..137
3012      KEY@116..123 "Depends"
3013      COLON@123..124 ":"
3014      NEWLINE@124..125 "\n"
3015      INDENT@125..126 " "
3016      VALUE@126..130 "bar,"
3017      NEWLINE@130..131 "\n"
3018      INDENT@131..132 " "
3019      VALUE@132..136 "blah"
3020      NEWLINE@136..137 "\n"
3021    ENTRY@137..203
3022      KEY@137..148 "Description"
3023      COLON@148..149 ":"
3024      WHITESPACE@149..150 " "
3025      VALUE@150..171 "This is a description"
3026      NEWLINE@171..172 "\n"
3027      INDENT@172..173 " "
3028      VALUE@173..182 "And it is"
3029      NEWLINE@182..183 "\n"
3030      INDENT@183..184 " "
3031      VALUE@184..185 "."
3032      NEWLINE@185..186 "\n"
3033      INDENT@186..187 " "
3034      VALUE@187..195 "multiple"
3035      NEWLINE@195..196 "\n"
3036      INDENT@196..197 " "
3037      VALUE@197..202 "lines"
3038      NEWLINE@202..203 "\n"
3039"###
3040    );
3041    assert_eq!(parsed.errors, Vec::<String>::new());
3042
3043    let root = parsed.root_mut();
3044    assert_eq!(root.paragraphs().count(), 2);
3045    let source = root.paragraphs().next().unwrap();
3046    assert_eq!(
3047        source.keys().collect::<Vec<_>>(),
3048        vec!["Source", "Maintainer", "Section"]
3049    );
3050    assert_eq!(source.get("Source").as_deref(), Some("foo"));
3051    assert_eq!(
3052        source.get("Maintainer").as_deref(),
3053        Some("Foo Bar <foo@example.com>")
3054    );
3055    assert_eq!(source.get("Section").as_deref(), Some("net"));
3056    assert_eq!(
3057        source.items().collect::<Vec<_>>(),
3058        vec![
3059            ("Source".into(), "foo".into()),
3060            ("Maintainer".into(), "Foo Bar <foo@example.com>".into()),
3061            ("Section".into(), "net".into()),
3062        ]
3063    );
3064
3065    let binary = root.paragraphs().nth(1).unwrap();
3066    assert_eq!(
3067        binary.keys().collect::<Vec<_>>(),
3068        vec!["Package", "Architecture", "Depends", "Description"]
3069    );
3070    assert_eq!(binary.get("Package").as_deref(), Some("foo"));
3071    assert_eq!(binary.get("Architecture").as_deref(), Some("all"));
3072    assert_eq!(binary.get("Depends").as_deref(), Some("bar,\nblah"));
3073    assert_eq!(
3074        binary.get("Description").as_deref(),
3075        Some("This is a description\nAnd it is\n.\nmultiple\nlines")
3076    );
3077
3078    assert_eq!(node.text(), CONTROLV1);
3079}
3080
3081#[test]
3082fn test_with_trailing_whitespace() {
3083    const CONTROLV1: &str = r#"Source: foo
3084Maintainer: Foo Bar <foo@example.com>
3085
3086
3087"#;
3088    let parsed = parse(CONTROLV1);
3089    let node = parsed.syntax();
3090    assert_eq!(
3091        format!("{:#?}", node),
3092        r###"ROOT@0..52
3093  PARAGRAPH@0..50
3094    ENTRY@0..12
3095      KEY@0..6 "Source"
3096      COLON@6..7 ":"
3097      WHITESPACE@7..8 " "
3098      VALUE@8..11 "foo"
3099      NEWLINE@11..12 "\n"
3100    ENTRY@12..50
3101      KEY@12..22 "Maintainer"
3102      COLON@22..23 ":"
3103      WHITESPACE@23..24 " "
3104      VALUE@24..49 "Foo Bar <foo@example. ..."
3105      NEWLINE@49..50 "\n"
3106  EMPTY_LINE@50..51
3107    NEWLINE@50..51 "\n"
3108  EMPTY_LINE@51..52
3109    NEWLINE@51..52 "\n"
3110"###
3111    );
3112    assert_eq!(parsed.errors, Vec::<String>::new());
3113
3114    let root = parsed.root_mut();
3115    assert_eq!(root.paragraphs().count(), 1);
3116    let source = root.paragraphs().next().unwrap();
3117    assert_eq!(
3118        source.items().collect::<Vec<_>>(),
3119        vec![
3120            ("Source".into(), "foo".into()),
3121            ("Maintainer".into(), "Foo Bar <foo@example.com>".into()),
3122        ]
3123    );
3124}
3125
3126fn rebuild_value(
3127    builder: &mut GreenNodeBuilder,
3128    mut tokens: Vec<(SyntaxKind, String)>,
3129    key_len: usize,
3130    indentation: u32,
3131    immediate_empty_line: bool,
3132    max_line_length_one_liner: Option<usize>,
3133) {
3134    let first_line_len = tokens
3135        .iter()
3136        .take_while(|(k, _t)| *k != NEWLINE)
3137        .map(|(_k, t)| t.len())
3138        .sum::<usize>() + key_len + 2 /* ": " */;
3139
3140    let has_newline = tokens.iter().any(|(k, _t)| *k == NEWLINE);
3141
3142    let mut last_was_newline = false;
3143    if max_line_length_one_liner
3144        .map(|mll| first_line_len <= mll)
3145        .unwrap_or(false)
3146        && !has_newline
3147    {
3148        // Just copy tokens if the value fits into one line
3149        for (k, t) in tokens {
3150            builder.token(k.into(), &t);
3151        }
3152    } else {
3153        // Insert a leading newline if the value is multi-line and immediate_empty_line is set
3154        if immediate_empty_line && has_newline {
3155            builder.token(NEWLINE.into(), "\n");
3156            last_was_newline = true;
3157        } else {
3158            builder.token(WHITESPACE.into(), " ");
3159        }
3160        // Strip leading whitespace and newlines
3161        let mut start_idx = 0;
3162        while start_idx < tokens.len() {
3163            if tokens[start_idx].0 == NEWLINE || tokens[start_idx].0 == WHITESPACE {
3164                start_idx += 1;
3165            } else {
3166                break;
3167            }
3168        }
3169        tokens.drain(..start_idx);
3170        // Pre-allocate indentation string to avoid repeated allocations
3171        let indent_str = " ".repeat(indentation as usize);
3172        for (k, t) in tokens {
3173            if last_was_newline {
3174                builder.token(INDENT.into(), &indent_str);
3175            }
3176            builder.token(k.into(), &t);
3177            last_was_newline = k == NEWLINE;
3178        }
3179    }
3180
3181    if !last_was_newline {
3182        builder.token(NEWLINE.into(), "\n");
3183    }
3184}
3185
3186#[cfg(test)]
3187mod tests {
3188    use super::*;
3189    #[test]
3190    fn test_parse() {
3191        let d: super::Deb822 = r#"Source: foo
3192Maintainer: Foo Bar <jelmer@jelmer.uk>
3193Section: net
3194
3195Package: foo
3196Architecture: all
3197Depends: libc6
3198Description: This is a description
3199 With details
3200"#
3201        .parse()
3202        .unwrap();
3203        let mut ps = d.paragraphs();
3204        let p = ps.next().unwrap();
3205
3206        assert_eq!(p.get("Source").as_deref(), Some("foo"));
3207        assert_eq!(
3208            p.get("Maintainer").as_deref(),
3209            Some("Foo Bar <jelmer@jelmer.uk>")
3210        );
3211        assert_eq!(p.get("Section").as_deref(), Some("net"));
3212
3213        let b = ps.next().unwrap();
3214        assert_eq!(b.get("Package").as_deref(), Some("foo"));
3215    }
3216
3217    #[test]
3218    fn test_after_multi_line() {
3219        let d: super::Deb822 = r#"Source: golang-github-blah-blah
3220Section: devel
3221Priority: optional
3222Standards-Version: 4.2.0
3223Maintainer: Some Maintainer <example@example.com>
3224Build-Depends: debhelper (>= 11~),
3225               dh-golang,
3226               golang-any
3227Homepage: https://github.com/j-keck/arping
3228"#
3229        .parse()
3230        .unwrap();
3231        let mut ps = d.paragraphs();
3232        let p = ps.next().unwrap();
3233        assert_eq!(p.get("Source").as_deref(), Some("golang-github-blah-blah"));
3234        assert_eq!(p.get("Section").as_deref(), Some("devel"));
3235        assert_eq!(p.get("Priority").as_deref(), Some("optional"));
3236        assert_eq!(p.get("Standards-Version").as_deref(), Some("4.2.0"));
3237        assert_eq!(
3238            p.get("Maintainer").as_deref(),
3239            Some("Some Maintainer <example@example.com>")
3240        );
3241        assert_eq!(
3242            p.get("Build-Depends").as_deref(),
3243            Some("debhelper (>= 11~),\ndh-golang,\ngolang-any")
3244        );
3245        assert_eq!(
3246            p.get("Homepage").as_deref(),
3247            Some("https://github.com/j-keck/arping")
3248        );
3249    }
3250
3251    #[test]
3252    fn test_remove_field() {
3253        let d: super::Deb822 = r#"Source: foo
3254# Comment
3255Maintainer: Foo Bar <jelmer@jelmer.uk>
3256Section: net
3257
3258Package: foo
3259Architecture: all
3260Depends: libc6
3261Description: This is a description
3262 With details
3263"#
3264        .parse()
3265        .unwrap();
3266        let mut ps = d.paragraphs();
3267        let mut p = ps.next().unwrap();
3268        p.set("Foo", "Bar");
3269        p.remove("Section");
3270        p.remove("Nonexistent");
3271        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3272        assert_eq!(
3273            p.to_string(),
3274            r#"Source: foo
3275# Comment
3276Maintainer: Foo Bar <jelmer@jelmer.uk>
3277Foo: Bar
3278"#
3279        );
3280    }
3281
3282    #[test]
3283    fn test_rename_field() {
3284        let d: super::Deb822 = r#"Source: foo
3285Vcs-Browser: https://salsa.debian.org/debian/foo
3286"#
3287        .parse()
3288        .unwrap();
3289        let mut ps = d.paragraphs();
3290        let mut p = ps.next().unwrap();
3291        assert!(p.rename("Vcs-Browser", "Homepage"));
3292        assert_eq!(
3293            p.to_string(),
3294            r#"Source: foo
3295Homepage: https://salsa.debian.org/debian/foo
3296"#
3297        );
3298
3299        assert_eq!(
3300            p.get("Homepage").as_deref(),
3301            Some("https://salsa.debian.org/debian/foo")
3302        );
3303        assert_eq!(p.get("Vcs-Browser").as_deref(), None);
3304
3305        // Nonexistent field
3306        assert!(!p.rename("Nonexistent", "Homepage"));
3307    }
3308
3309    #[test]
3310    fn test_set_field() {
3311        let d: super::Deb822 = r#"Source: foo
3312Maintainer: Foo Bar <joe@example.com>
3313"#
3314        .parse()
3315        .unwrap();
3316        let mut ps = d.paragraphs();
3317        let mut p = ps.next().unwrap();
3318        p.set("Maintainer", "Somebody Else <jane@example.com>");
3319        assert_eq!(
3320            p.get("Maintainer").as_deref(),
3321            Some("Somebody Else <jane@example.com>")
3322        );
3323        assert_eq!(
3324            p.to_string(),
3325            r#"Source: foo
3326Maintainer: Somebody Else <jane@example.com>
3327"#
3328        );
3329    }
3330
3331    #[test]
3332    fn test_set_new_field() {
3333        let d: super::Deb822 = r#"Source: foo
3334"#
3335        .parse()
3336        .unwrap();
3337        let mut ps = d.paragraphs();
3338        let mut p = ps.next().unwrap();
3339        p.set("Maintainer", "Somebody <joe@example.com>");
3340        assert_eq!(
3341            p.get("Maintainer").as_deref(),
3342            Some("Somebody <joe@example.com>")
3343        );
3344        assert_eq!(
3345            p.to_string(),
3346            r#"Source: foo
3347Maintainer: Somebody <joe@example.com>
3348"#
3349        );
3350    }
3351
3352    #[test]
3353    fn test_add_paragraph() {
3354        let mut d = super::Deb822::new();
3355        let mut p = d.add_paragraph();
3356        p.set("Foo", "Bar");
3357        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3358        assert_eq!(
3359            p.to_string(),
3360            r#"Foo: Bar
3361"#
3362        );
3363        assert_eq!(
3364            d.to_string(),
3365            r#"Foo: Bar
3366"#
3367        );
3368
3369        let mut p = d.add_paragraph();
3370        p.set("Foo", "Blah");
3371        assert_eq!(p.get("Foo").as_deref(), Some("Blah"));
3372        assert_eq!(
3373            d.to_string(),
3374            r#"Foo: Bar
3375
3376Foo: Blah
3377"#
3378        );
3379    }
3380
3381    #[test]
3382    fn test_crud_paragraph() {
3383        let mut d = super::Deb822::new();
3384        let mut p = d.insert_paragraph(0);
3385        p.set("Foo", "Bar");
3386        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3387        assert_eq!(
3388            d.to_string(),
3389            r#"Foo: Bar
3390"#
3391        );
3392
3393        // test prepend
3394        let mut p = d.insert_paragraph(0);
3395        p.set("Foo", "Blah");
3396        assert_eq!(p.get("Foo").as_deref(), Some("Blah"));
3397        assert_eq!(
3398            d.to_string(),
3399            r#"Foo: Blah
3400
3401Foo: Bar
3402"#
3403        );
3404
3405        // test delete
3406        d.remove_paragraph(1);
3407        assert_eq!(d.to_string(), "Foo: Blah\n\n");
3408
3409        // test update again
3410        p.set("Foo", "Baz");
3411        assert_eq!(d.to_string(), "Foo: Baz\n\n");
3412
3413        // test delete again
3414        d.remove_paragraph(0);
3415        assert_eq!(d.to_string(), "");
3416    }
3417
3418    #[test]
3419    fn test_swap_paragraphs() {
3420        // Test basic swap
3421        let mut d: super::Deb822 = vec![
3422            vec![("Foo", "Bar")].into_iter().collect(),
3423            vec![("A", "B")].into_iter().collect(),
3424            vec![("X", "Y")].into_iter().collect(),
3425        ]
3426        .into_iter()
3427        .collect();
3428
3429        d.swap_paragraphs(0, 2);
3430        assert_eq!(d.to_string(), "X: Y\n\nA: B\n\nFoo: Bar\n");
3431
3432        // Swap back
3433        d.swap_paragraphs(0, 2);
3434        assert_eq!(d.to_string(), "Foo: Bar\n\nA: B\n\nX: Y\n");
3435
3436        // Swap adjacent paragraphs
3437        d.swap_paragraphs(0, 1);
3438        assert_eq!(d.to_string(), "A: B\n\nFoo: Bar\n\nX: Y\n");
3439
3440        // Swap with same index should be no-op
3441        let before = d.to_string();
3442        d.swap_paragraphs(1, 1);
3443        assert_eq!(d.to_string(), before);
3444    }
3445
3446    #[test]
3447    fn test_swap_paragraphs_preserves_content() {
3448        // Test that field content is preserved
3449        let mut d: super::Deb822 = vec![
3450            vec![("Field1", "Value1"), ("Field2", "Value2")]
3451                .into_iter()
3452                .collect(),
3453            vec![("FieldA", "ValueA"), ("FieldB", "ValueB")]
3454                .into_iter()
3455                .collect(),
3456        ]
3457        .into_iter()
3458        .collect();
3459
3460        d.swap_paragraphs(0, 1);
3461
3462        let mut paras = d.paragraphs();
3463        let p1 = paras.next().unwrap();
3464        assert_eq!(p1.get("FieldA").as_deref(), Some("ValueA"));
3465        assert_eq!(p1.get("FieldB").as_deref(), Some("ValueB"));
3466
3467        let p2 = paras.next().unwrap();
3468        assert_eq!(p2.get("Field1").as_deref(), Some("Value1"));
3469        assert_eq!(p2.get("Field2").as_deref(), Some("Value2"));
3470    }
3471
3472    #[test]
3473    #[should_panic(expected = "out of bounds")]
3474    fn test_swap_paragraphs_out_of_bounds() {
3475        let mut d: super::Deb822 = vec![
3476            vec![("Foo", "Bar")].into_iter().collect(),
3477            vec![("A", "B")].into_iter().collect(),
3478        ]
3479        .into_iter()
3480        .collect();
3481
3482        d.swap_paragraphs(0, 5);
3483    }
3484
3485    #[test]
3486    fn test_multiline_entry() {
3487        use super::SyntaxKind::*;
3488        use rowan::ast::AstNode;
3489
3490        let entry = super::Entry::new("foo", "bar\nbaz");
3491        let tokens: Vec<_> = entry
3492            .syntax()
3493            .descendants_with_tokens()
3494            .filter_map(|tok| tok.into_token())
3495            .collect();
3496
3497        assert_eq!("foo: bar\n baz\n", entry.to_string());
3498        assert_eq!("bar\nbaz", entry.value());
3499
3500        assert_eq!(
3501            vec![
3502                (KEY, "foo"),
3503                (COLON, ":"),
3504                (WHITESPACE, " "),
3505                (VALUE, "bar"),
3506                (NEWLINE, "\n"),
3507                (INDENT, " "),
3508                (VALUE, "baz"),
3509                (NEWLINE, "\n"),
3510            ],
3511            tokens
3512                .iter()
3513                .map(|token| (token.kind(), token.text()))
3514                .collect::<Vec<_>>()
3515        );
3516    }
3517
3518    #[test]
3519    fn test_apt_entry() {
3520        let text = r#"Package: cvsd
3521Binary: cvsd
3522Version: 1.0.24
3523Maintainer: Arthur de Jong <adejong@debian.org>
3524Build-Depends: debhelper (>= 9), po-debconf
3525Architecture: any
3526Standards-Version: 3.9.3
3527Format: 3.0 (native)
3528Files:
3529 b7a7d67a02974c52c408fdb5e118406d 890 cvsd_1.0.24.dsc
3530 b73ee40774c3086cb8490cdbb96ac883 258139 cvsd_1.0.24.tar.gz
3531Vcs-Browser: http://arthurdejong.org/viewvc/cvsd/
3532Vcs-Cvs: :pserver:anonymous@arthurdejong.org:/arthur/
3533Checksums-Sha256:
3534 a7bb7a3aacee19cd14ce5c26cb86e348b1608e6f1f6e97c6ea7c58efa440ac43 890 cvsd_1.0.24.dsc
3535 46bc517760c1070ae408693b89603986b53e6f068ae6bdc744e2e830e46b8cba 258139 cvsd_1.0.24.tar.gz
3536Homepage: http://arthurdejong.org/cvsd/
3537Package-List:
3538 cvsd deb vcs optional
3539Directory: pool/main/c/cvsd
3540Priority: source
3541Section: vcs
3542
3543"#;
3544        let d: super::Deb822 = text.parse().unwrap();
3545        let p = d.paragraphs().next().unwrap();
3546        assert_eq!(p.get("Binary").as_deref(), Some("cvsd"));
3547        assert_eq!(p.get("Version").as_deref(), Some("1.0.24"));
3548        assert_eq!(
3549            p.get("Maintainer").as_deref(),
3550            Some("Arthur de Jong <adejong@debian.org>")
3551        );
3552    }
3553
3554    #[test]
3555    fn test_format() {
3556        let d: super::Deb822 = r#"Source: foo
3557Maintainer: Foo Bar <foo@example.com>
3558Section:      net
3559Blah: blah  # comment
3560Multi-Line:
3561  Ahoi!
3562     Matey!
3563
3564"#
3565        .parse()
3566        .unwrap();
3567        let mut ps = d.paragraphs();
3568        let p = ps.next().unwrap();
3569        let result = p.wrap_and_sort(
3570            crate::Indentation::FieldNameLength,
3571            false,
3572            None,
3573            None::<&dyn Fn(&super::Entry, &super::Entry) -> std::cmp::Ordering>,
3574            None,
3575        );
3576        assert_eq!(
3577            result.to_string(),
3578            r#"Source: foo
3579Maintainer: Foo Bar <foo@example.com>
3580Section: net
3581Blah: blah  # comment
3582Multi-Line: Ahoi!
3583          Matey!
3584"#
3585        );
3586    }
3587
3588    #[test]
3589    fn test_format_sort_paragraphs() {
3590        let d: super::Deb822 = r#"Source: foo
3591Maintainer: Foo Bar <foo@example.com>
3592
3593# This is a comment
3594Source: bar
3595Maintainer: Bar Foo <bar@example.com>
3596
3597"#
3598        .parse()
3599        .unwrap();
3600        let result = d.wrap_and_sort(
3601            Some(&|a: &super::Paragraph, b: &super::Paragraph| {
3602                a.get("Source").cmp(&b.get("Source"))
3603            }),
3604            Some(&|p| {
3605                p.wrap_and_sort(
3606                    crate::Indentation::FieldNameLength,
3607                    false,
3608                    None,
3609                    None::<&dyn Fn(&super::Entry, &super::Entry) -> std::cmp::Ordering>,
3610                    None,
3611                )
3612            }),
3613        );
3614        assert_eq!(
3615            result.to_string(),
3616            r#"# This is a comment
3617Source: bar
3618Maintainer: Bar Foo <bar@example.com>
3619
3620Source: foo
3621Maintainer: Foo Bar <foo@example.com>
3622"#,
3623        );
3624    }
3625
3626    #[test]
3627    fn test_format_sort_fields() {
3628        let d: super::Deb822 = r#"Source: foo
3629Maintainer: Foo Bar <foo@example.com>
3630Build-Depends: debhelper (>= 9), po-debconf
3631Homepage: https://example.com/
3632
3633"#
3634        .parse()
3635        .unwrap();
3636        let result = d.wrap_and_sort(
3637            None,
3638            Some(&|p: &super::Paragraph| -> super::Paragraph {
3639                p.wrap_and_sort(
3640                    crate::Indentation::FieldNameLength,
3641                    false,
3642                    None,
3643                    Some(&|a: &super::Entry, b: &super::Entry| a.key().cmp(&b.key())),
3644                    None,
3645                )
3646            }),
3647        );
3648        assert_eq!(
3649            result.to_string(),
3650            r#"Build-Depends: debhelper (>= 9), po-debconf
3651Homepage: https://example.com/
3652Maintainer: Foo Bar <foo@example.com>
3653Source: foo
3654"#
3655        );
3656    }
3657
3658    #[test]
3659    fn test_para_from_iter() {
3660        let p: super::Paragraph = vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect();
3661        assert_eq!(
3662            p.to_string(),
3663            r#"Foo: Bar
3664Baz: Qux
3665"#
3666        );
3667
3668        let p: super::Paragraph = vec![
3669            ("Foo".to_string(), "Bar".to_string()),
3670            ("Baz".to_string(), "Qux".to_string()),
3671        ]
3672        .into_iter()
3673        .collect();
3674
3675        assert_eq!(
3676            p.to_string(),
3677            r#"Foo: Bar
3678Baz: Qux
3679"#
3680        );
3681    }
3682
3683    #[test]
3684    fn test_deb822_from_iter() {
3685        let d: super::Deb822 = vec![
3686            vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
3687            vec![("A", "B"), ("C", "D")].into_iter().collect(),
3688        ]
3689        .into_iter()
3690        .collect();
3691        assert_eq!(
3692            d.to_string(),
3693            r#"Foo: Bar
3694Baz: Qux
3695
3696A: B
3697C: D
3698"#
3699        );
3700    }
3701
3702    #[test]
3703    fn test_format_parse_error() {
3704        assert_eq!(ParseError(vec!["foo".to_string()]).to_string(), "foo\n");
3705    }
3706
3707    #[test]
3708    fn test_set_with_field_order() {
3709        let mut p = super::Paragraph::new();
3710        let custom_order = &["Foo", "Bar", "Baz"];
3711
3712        p.set_with_field_order("Baz", "3", custom_order);
3713        p.set_with_field_order("Foo", "1", custom_order);
3714        p.set_with_field_order("Bar", "2", custom_order);
3715        p.set_with_field_order("Unknown", "4", custom_order);
3716
3717        let keys: Vec<_> = p.keys().collect();
3718        assert_eq!(keys[0], "Foo");
3719        assert_eq!(keys[1], "Bar");
3720        assert_eq!(keys[2], "Baz");
3721        assert_eq!(keys[3], "Unknown");
3722    }
3723
3724    #[test]
3725    fn test_positioned_parse_error() {
3726        let error = PositionedParseError {
3727            message: "test error".to_string(),
3728            range: rowan::TextRange::new(rowan::TextSize::from(5), rowan::TextSize::from(10)),
3729            code: Some("test_code".to_string()),
3730        };
3731        assert_eq!(error.to_string(), "test error");
3732        assert_eq!(error.range.start(), rowan::TextSize::from(5));
3733        assert_eq!(error.range.end(), rowan::TextSize::from(10));
3734        assert_eq!(error.code, Some("test_code".to_string()));
3735    }
3736
3737    #[test]
3738    fn test_format_error() {
3739        assert_eq!(
3740            super::Error::ParseError(ParseError(vec!["foo".to_string()])).to_string(),
3741            "foo\n"
3742        );
3743    }
3744
3745    #[test]
3746    fn test_get_all() {
3747        let d: super::Deb822 = r#"Source: foo
3748Maintainer: Foo Bar <foo@example.com>
3749Maintainer: Bar Foo <bar@example.com>"#
3750            .parse()
3751            .unwrap();
3752        let p = d.paragraphs().next().unwrap();
3753        assert_eq!(
3754            p.get_all("Maintainer").collect::<Vec<_>>(),
3755            vec!["Foo Bar <foo@example.com>", "Bar Foo <bar@example.com>"]
3756        );
3757    }
3758
3759    #[test]
3760    fn test_get_with_indent_single_line() {
3761        let input = "Field: single line value\n";
3762        let deb = super::Deb822::from_str(input).unwrap();
3763        let para = deb.paragraphs().next().unwrap();
3764
3765        // Single-line values should be unchanged regardless of indent pattern
3766        assert_eq!(
3767            para.get_with_indent("Field", &super::IndentPattern::Fixed(2)),
3768            Some("single line value".to_string())
3769        );
3770        assert_eq!(
3771            para.get_with_indent("Field", &super::IndentPattern::FieldNameLength),
3772            Some("single line value".to_string())
3773        );
3774    }
3775
3776    #[test]
3777    fn test_get_with_indent_fixed() {
3778        let input = "Field: First\n   Second\n   Third\n";
3779        let deb = super::Deb822::from_str(input).unwrap();
3780        let para = deb.paragraphs().next().unwrap();
3781
3782        // Get with fixed 2-space indentation - strips 2 spaces, leaves 1
3783        let value = para
3784            .get_with_indent("Field", &super::IndentPattern::Fixed(2))
3785            .unwrap();
3786        assert_eq!(value, "First\n Second\n Third");
3787
3788        // Get with fixed 1-space indentation - strips 1 space, leaves 2
3789        let value = para
3790            .get_with_indent("Field", &super::IndentPattern::Fixed(1))
3791            .unwrap();
3792        assert_eq!(value, "First\n  Second\n  Third");
3793
3794        // Get with fixed 3-space indentation - strips all 3 spaces
3795        let value = para
3796            .get_with_indent("Field", &super::IndentPattern::Fixed(3))
3797            .unwrap();
3798        assert_eq!(value, "First\nSecond\nThird");
3799    }
3800
3801    #[test]
3802    fn test_get_with_indent_field_name_length() {
3803        let input = "Description: First line\n             Second line\n             Third line\n";
3804        let deb = super::Deb822::from_str(input).unwrap();
3805        let para = deb.paragraphs().next().unwrap();
3806
3807        // Get with FieldNameLength pattern
3808        // "Description: " is 13 characters, so strips 13 spaces, leaves 0
3809        let value = para
3810            .get_with_indent("Description", &super::IndentPattern::FieldNameLength)
3811            .unwrap();
3812        assert_eq!(value, "First line\nSecond line\nThird line");
3813
3814        // Get with fixed 2-space indentation - strips 2, leaves 11
3815        let value = para
3816            .get_with_indent("Description", &super::IndentPattern::Fixed(2))
3817            .unwrap();
3818        assert_eq!(
3819            value,
3820            "First line\n           Second line\n           Third line"
3821        );
3822    }
3823
3824    #[test]
3825    fn test_get_with_indent_nonexistent() {
3826        let input = "Field: value\n";
3827        let deb = super::Deb822::from_str(input).unwrap();
3828        let para = deb.paragraphs().next().unwrap();
3829
3830        assert_eq!(
3831            para.get_with_indent("NonExistent", &super::IndentPattern::Fixed(2)),
3832            None
3833        );
3834    }
3835
3836    #[test]
3837    fn test_get_entry() {
3838        let input = r#"Package: test-package
3839Maintainer: Test User <test@example.com>
3840Description: A simple test package
3841 with multiple lines
3842"#;
3843        let deb = super::Deb822::from_str(input).unwrap();
3844        let para = deb.paragraphs().next().unwrap();
3845
3846        // Test getting existing entry
3847        let entry = para.get_entry("Package");
3848        assert!(entry.is_some());
3849        let entry = entry.unwrap();
3850        assert_eq!(entry.key(), Some("Package".to_string()));
3851        assert_eq!(entry.value(), "test-package");
3852
3853        // Test case-insensitive lookup
3854        let entry = para.get_entry("package");
3855        assert!(entry.is_some());
3856        assert_eq!(entry.unwrap().value(), "test-package");
3857
3858        // Test multi-line value
3859        let entry = para.get_entry("Description");
3860        assert!(entry.is_some());
3861        assert_eq!(
3862            entry.unwrap().value(),
3863            "A simple test package\nwith multiple lines"
3864        );
3865
3866        // Test non-existent field
3867        assert_eq!(para.get_entry("NonExistent"), None);
3868    }
3869
3870    #[test]
3871    fn test_entry_ranges() {
3872        let input = r#"Package: test-package
3873Maintainer: Test User <test@example.com>
3874Description: A simple test package
3875 with multiple lines
3876 of description text"#;
3877
3878        let deb822 = super::Deb822::from_str(input).unwrap();
3879        let paragraph = deb822.paragraphs().next().unwrap();
3880        let entries: Vec<_> = paragraph.entries().collect();
3881
3882        // Test first entry (Package)
3883        let package_entry = &entries[0];
3884        assert_eq!(package_entry.key(), Some("Package".to_string()));
3885
3886        // Test key_range
3887        let key_range = package_entry.key_range().unwrap();
3888        assert_eq!(
3889            &input[key_range.start().into()..key_range.end().into()],
3890            "Package"
3891        );
3892
3893        // Test colon_range
3894        let colon_range = package_entry.colon_range().unwrap();
3895        assert_eq!(
3896            &input[colon_range.start().into()..colon_range.end().into()],
3897            ":"
3898        );
3899
3900        // Test value_range
3901        let value_range = package_entry.value_range().unwrap();
3902        assert_eq!(
3903            &input[value_range.start().into()..value_range.end().into()],
3904            "test-package"
3905        );
3906
3907        // Test text_range covers the whole entry
3908        let text_range = package_entry.text_range();
3909        assert_eq!(
3910            &input[text_range.start().into()..text_range.end().into()],
3911            "Package: test-package\n"
3912        );
3913
3914        // Test single-line value_line_ranges
3915        let value_lines = package_entry.value_line_ranges();
3916        assert_eq!(value_lines.len(), 1);
3917        assert_eq!(
3918            &input[value_lines[0].start().into()..value_lines[0].end().into()],
3919            "test-package"
3920        );
3921
3922        // Test value_token_range: single-word value covers the whole value.
3923        let token_range = package_entry.value_token_range().unwrap();
3924        assert_eq!(
3925            &input[token_range.start().into()..token_range.end().into()],
3926            "test-package"
3927        );
3928    }
3929
3930    #[test]
3931    fn test_value_token_range_multiword() {
3932        let input = "License: GPL-2+ with the autoconf exception\n";
3933        let para = Deb822::from_str(input)
3934            .unwrap()
3935            .paragraphs()
3936            .next()
3937            .unwrap();
3938        let entry = para.entries().next().unwrap();
3939        let token_range = entry.value_token_range().unwrap();
3940        // Only the first whitespace-delimited token (the short-name).
3941        assert_eq!(
3942            &input[token_range.start().into()..token_range.end().into()],
3943            "GPL-2+"
3944        );
3945    }
3946
3947    #[test]
3948    fn test_value_token_range_empty() {
3949        let input = "Description:\n";
3950        let para = Deb822::from_str(input)
3951            .unwrap()
3952            .paragraphs()
3953            .next()
3954            .unwrap();
3955        let entry = para.entries().next().unwrap();
3956        assert_eq!(entry.value_token_range(), None);
3957    }
3958
3959    #[test]
3960    fn test_multiline_entry_ranges() {
3961        let input = r#"Description: Short description
3962 Extended description line 1
3963 Extended description line 2"#;
3964
3965        let deb822 = super::Deb822::from_str(input).unwrap();
3966        let paragraph = deb822.paragraphs().next().unwrap();
3967        let entry = paragraph.entries().next().unwrap();
3968
3969        assert_eq!(entry.key(), Some("Description".to_string()));
3970
3971        // Test value_range spans all lines
3972        let value_range = entry.value_range().unwrap();
3973        let full_value = &input[value_range.start().into()..value_range.end().into()];
3974        assert!(full_value.contains("Short description"));
3975        assert!(full_value.contains("Extended description line 1"));
3976        assert!(full_value.contains("Extended description line 2"));
3977
3978        // Test value_line_ranges gives individual lines
3979        let value_lines = entry.value_line_ranges();
3980        assert_eq!(value_lines.len(), 3);
3981
3982        assert_eq!(
3983            &input[value_lines[0].start().into()..value_lines[0].end().into()],
3984            "Short description"
3985        );
3986        assert_eq!(
3987            &input[value_lines[1].start().into()..value_lines[1].end().into()],
3988            "Extended description line 1"
3989        );
3990        assert_eq!(
3991            &input[value_lines[2].start().into()..value_lines[2].end().into()],
3992            "Extended description line 2"
3993        );
3994    }
3995
3996    #[test]
3997    fn test_entries_public_access() {
3998        let input = r#"Package: test
3999Version: 1.0"#;
4000
4001        let deb822 = super::Deb822::from_str(input).unwrap();
4002        let paragraph = deb822.paragraphs().next().unwrap();
4003
4004        // Test that entries() method is now public
4005        let entries: Vec<_> = paragraph.entries().collect();
4006        assert_eq!(entries.len(), 2);
4007        assert_eq!(entries[0].key(), Some("Package".to_string()));
4008        assert_eq!(entries[1].key(), Some("Version".to_string()));
4009    }
4010
4011    #[test]
4012    fn test_empty_value_ranges() {
4013        let input = r#"EmptyField: "#;
4014
4015        let deb822 = super::Deb822::from_str(input).unwrap();
4016        let paragraph = deb822.paragraphs().next().unwrap();
4017        let entry = paragraph.entries().next().unwrap();
4018
4019        assert_eq!(entry.key(), Some("EmptyField".to_string()));
4020
4021        // Empty value should still have ranges
4022        assert!(entry.key_range().is_some());
4023        assert!(entry.colon_range().is_some());
4024
4025        // Empty value might not have value tokens
4026        let value_lines = entry.value_line_ranges();
4027        // This depends on how the parser handles empty values
4028        // but we should not panic
4029        assert!(value_lines.len() <= 1);
4030    }
4031
4032    #[test]
4033    fn test_range_ordering() {
4034        let input = r#"Field: value"#;
4035
4036        let deb822 = super::Deb822::from_str(input).unwrap();
4037        let paragraph = deb822.paragraphs().next().unwrap();
4038        let entry = paragraph.entries().next().unwrap();
4039
4040        let key_range = entry.key_range().unwrap();
4041        let colon_range = entry.colon_range().unwrap();
4042        let value_range = entry.value_range().unwrap();
4043        let text_range = entry.text_range();
4044
4045        // Verify ranges are in correct order
4046        assert!(key_range.end() <= colon_range.start());
4047        assert!(colon_range.end() <= value_range.start());
4048        assert!(key_range.start() >= text_range.start());
4049        assert!(value_range.end() <= text_range.end());
4050    }
4051
4052    #[test]
4053    fn test_error_recovery_missing_colon() {
4054        let input = r#"Source foo
4055Maintainer: Test User <test@example.com>
4056"#;
4057        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4058
4059        // Should still parse successfully with errors
4060        assert!(!errors.is_empty());
4061        assert!(errors.iter().any(|e| e.contains("missing colon")));
4062
4063        // Should still have a paragraph with the valid field
4064        let paragraph = deb822.paragraphs().next().unwrap();
4065        assert_eq!(
4066            paragraph.get("Maintainer").as_deref(),
4067            Some("Test User <test@example.com>")
4068        );
4069    }
4070
4071    #[test]
4072    fn test_error_recovery_missing_field_name() {
4073        let input = r#": orphaned value
4074Package: test
4075"#;
4076
4077        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4078
4079        // Should have errors about missing field name
4080        assert!(!errors.is_empty());
4081        assert!(errors
4082            .iter()
4083            .any(|e| e.contains("field name") || e.contains("missing")));
4084
4085        // The valid field should be in one of the paragraphs
4086        let paragraphs: Vec<_> = deb822.paragraphs().collect();
4087        let mut found_package = false;
4088        for paragraph in paragraphs.iter() {
4089            if paragraph.get("Package").is_some() {
4090                found_package = true;
4091                assert_eq!(paragraph.get("Package").as_deref(), Some("test"));
4092            }
4093        }
4094        assert!(found_package, "Package field not found in any paragraph");
4095    }
4096
4097    #[test]
4098    fn test_error_recovery_orphaned_text() {
4099        let input = r#"Package: test
4100some orphaned text without field name
4101Version: 1.0
4102"#;
4103        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4104
4105        // Should have errors about orphaned text
4106        assert!(!errors.is_empty());
4107        assert!(errors.iter().any(|e| e.contains("orphaned")
4108            || e.contains("unexpected")
4109            || e.contains("field name")));
4110
4111        // Should still parse the valid fields (may be split across paragraphs)
4112        let mut all_fields = std::collections::HashMap::new();
4113        for paragraph in deb822.paragraphs() {
4114            for (key, value) in paragraph.items() {
4115                all_fields.insert(key, value);
4116            }
4117        }
4118
4119        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
4120        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
4121    }
4122
4123    #[test]
4124    fn test_error_recovery_consecutive_field_names() {
4125        let input = r#"Package: test
4126Description
4127Maintainer: Another field without proper value
4128Version: 1.0
4129"#;
4130        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4131
4132        // Should have errors about missing values
4133        assert!(!errors.is_empty());
4134        assert!(errors.iter().any(|e| e.contains("consecutive")
4135            || e.contains("missing")
4136            || e.contains("incomplete")));
4137
4138        // Should still parse valid fields (may be split across paragraphs due to errors)
4139        let mut all_fields = std::collections::HashMap::new();
4140        for paragraph in deb822.paragraphs() {
4141            for (key, value) in paragraph.items() {
4142                all_fields.insert(key, value);
4143            }
4144        }
4145
4146        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
4147        assert_eq!(
4148            all_fields.get("Maintainer"),
4149            Some(&"Another field without proper value".to_string())
4150        );
4151        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
4152    }
4153
4154    #[test]
4155    fn test_error_recovery_malformed_multiline() {
4156        let input = r#"Package: test
4157Description: Short desc
4158  Proper continuation
4159invalid continuation without indent
4160 Another proper continuation
4161Version: 1.0
4162"#;
4163        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4164
4165        // Should recover from malformed continuation
4166        assert!(!errors.is_empty());
4167
4168        // Should still parse other fields correctly
4169        let paragraph = deb822.paragraphs().next().unwrap();
4170        assert_eq!(paragraph.get("Package").as_deref(), Some("test"));
4171        assert_eq!(paragraph.get("Version").as_deref(), Some("1.0"));
4172    }
4173
4174    #[test]
4175    fn test_error_recovery_mixed_errors() {
4176        let input = r#"Package test without colon
4177: orphaned colon
4178Description: Valid field
4179some orphaned text
4180Another-Field: Valid too
4181"#;
4182        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4183
4184        // Should have multiple different errors
4185        assert!(!errors.is_empty());
4186        assert!(errors.len() >= 2);
4187
4188        // Should still parse the valid fields
4189        let paragraph = deb822.paragraphs().next().unwrap();
4190        assert_eq!(paragraph.get("Description").as_deref(), Some("Valid field"));
4191        assert_eq!(paragraph.get("Another-Field").as_deref(), Some("Valid too"));
4192    }
4193
4194    #[test]
4195    fn test_error_recovery_paragraph_boundary() {
4196        let input = r#"Package: first-package
4197Description: First paragraph
4198
4199corrupted data here
4200: more corruption
4201completely broken line
4202
4203Package: second-package
4204Version: 1.0
4205"#;
4206        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4207
4208        // Should have errors from the corrupted section
4209        assert!(!errors.is_empty());
4210
4211        // Should still parse both paragraphs correctly
4212        let paragraphs: Vec<_> = deb822.paragraphs().collect();
4213        assert_eq!(paragraphs.len(), 2);
4214
4215        assert_eq!(
4216            paragraphs[0].get("Package").as_deref(),
4217            Some("first-package")
4218        );
4219        assert_eq!(
4220            paragraphs[1].get("Package").as_deref(),
4221            Some("second-package")
4222        );
4223        assert_eq!(paragraphs[1].get("Version").as_deref(), Some("1.0"));
4224    }
4225
4226    #[test]
4227    fn test_error_recovery_with_positioned_errors() {
4228        let input = r#"Package test
4229Description: Valid
4230"#;
4231        let parsed = super::parse(input);
4232
4233        // Should have positioned errors with proper ranges
4234        assert!(!parsed.positioned_errors.is_empty());
4235
4236        let first_error = &parsed.positioned_errors[0];
4237        assert!(!first_error.message.is_empty());
4238        assert!(first_error.range.start() <= first_error.range.end());
4239        assert!(first_error.code.is_some());
4240
4241        // Error should point to the problematic location
4242        let error_text = &input[first_error.range.start().into()..first_error.range.end().into()];
4243        assert!(!error_text.is_empty());
4244    }
4245
4246    #[test]
4247    fn test_positioned_error_points_to_correct_token() {
4248        let input = "Package test\nDescription: Valid\n";
4249        let parsed = super::parse(input);
4250
4251        assert_eq!(parsed.positioned_errors.len(), 1);
4252
4253        let first_error = &parsed.positioned_errors[0];
4254        assert_eq!(first_error.message, "missing colon ':' after field name");
4255        assert_eq!(first_error.code.as_deref(), Some("missing_colon"));
4256
4257        let start: usize = first_error.range.start().into();
4258        let end: usize = first_error.range.end().into();
4259        assert_eq!(start, 8);
4260        assert_eq!(end, 12);
4261        assert_eq!(&input[start..end], "test");
4262    }
4263
4264    #[test]
4265    fn test_error_recovery_preserves_whitespace() {
4266        let input = r#"Source: package
4267Maintainer   Test User <test@example.com>
4268Section:    utils
4269
4270"#;
4271        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4272
4273        // Should have error about missing colon
4274        assert!(!errors.is_empty());
4275
4276        // Should preserve original formatting in output
4277        let output = deb822.to_string();
4278        assert!(output.contains("Section:    utils"));
4279
4280        // Should still extract valid fields
4281        let paragraph = deb822.paragraphs().next().unwrap();
4282        assert_eq!(paragraph.get("Source").as_deref(), Some("package"));
4283        assert_eq!(paragraph.get("Section").as_deref(), Some("utils"));
4284    }
4285
4286    #[test]
4287    fn test_error_recovery_empty_fields() {
4288        let input = r#"Package: test
4289Description:
4290Maintainer: Valid User
4291EmptyField:
4292Version: 1.0
4293"#;
4294        let (deb822, _errors) = super::Deb822::from_str_relaxed(input);
4295
4296        // Empty fields should parse without major errors - collect all fields from all paragraphs
4297        let mut all_fields = std::collections::HashMap::new();
4298        for paragraph in deb822.paragraphs() {
4299            for (key, value) in paragraph.items() {
4300                all_fields.insert(key, value);
4301            }
4302        }
4303
4304        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
4305        assert_eq!(all_fields.get("Description"), Some(&"".to_string()));
4306        assert_eq!(
4307            all_fields.get("Maintainer"),
4308            Some(&"Valid User".to_string())
4309        );
4310        assert_eq!(all_fields.get("EmptyField"), Some(&"".to_string()));
4311        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
4312    }
4313
4314    #[test]
4315    fn test_insert_comment_before() {
4316        let d: super::Deb822 = vec![
4317            vec![("Source", "foo"), ("Maintainer", "Bar <bar@example.com>")]
4318                .into_iter()
4319                .collect(),
4320            vec![("Package", "foo"), ("Architecture", "all")]
4321                .into_iter()
4322                .collect(),
4323        ]
4324        .into_iter()
4325        .collect();
4326
4327        // Insert comment before first paragraph
4328        let mut p1 = d.paragraphs().next().unwrap();
4329        p1.insert_comment_before("This is the source paragraph");
4330
4331        // Insert comment before second paragraph
4332        let mut p2 = d.paragraphs().nth(1).unwrap();
4333        p2.insert_comment_before("This is the binary paragraph");
4334
4335        let output = d.to_string();
4336        assert_eq!(
4337            output,
4338            r#"# This is the source paragraph
4339Source: foo
4340Maintainer: Bar <bar@example.com>
4341
4342# This is the binary paragraph
4343Package: foo
4344Architecture: all
4345"#
4346        );
4347    }
4348
4349    #[test]
4350    fn test_parse_continuation_with_colon() {
4351        // Test that continuation lines with colons are properly parsed
4352        let input = "Package: test\nDescription: short\n line: with colon\n";
4353        let result = input.parse::<Deb822>();
4354        assert!(result.is_ok());
4355
4356        let deb822 = result.unwrap();
4357        let para = deb822.paragraphs().next().unwrap();
4358        assert_eq!(para.get("Package").as_deref(), Some("test"));
4359        assert_eq!(
4360            para.get("Description").as_deref(),
4361            Some("short\nline: with colon")
4362        );
4363    }
4364
4365    #[test]
4366    fn test_parse_continuation_starting_with_colon() {
4367        // Test continuation line STARTING with a colon (issue #315)
4368        let input = "Package: test\nDescription: short\n :value\n";
4369        let result = input.parse::<Deb822>();
4370        assert!(result.is_ok());
4371
4372        let deb822 = result.unwrap();
4373        let para = deb822.paragraphs().next().unwrap();
4374        assert_eq!(para.get("Package").as_deref(), Some("test"));
4375        assert_eq!(para.get("Description").as_deref(), Some("short\n:value"));
4376    }
4377
4378    #[test]
4379    fn test_normalize_field_spacing_single_space() {
4380        // Field already has correct spacing
4381        let input = "Field: value\n";
4382        let deb822 = input.parse::<Deb822>().unwrap();
4383        let mut para = deb822.paragraphs().next().unwrap();
4384
4385        para.normalize_field_spacing();
4386        assert_eq!(para.to_string(), "Field: value\n");
4387    }
4388
4389    #[test]
4390    fn test_normalize_field_spacing_extra_spaces() {
4391        // Field has extra spaces after colon
4392        let input = "Field:    value\n";
4393        let deb822 = input.parse::<Deb822>().unwrap();
4394        let mut para = deb822.paragraphs().next().unwrap();
4395
4396        para.normalize_field_spacing();
4397        assert_eq!(para.to_string(), "Field: value\n");
4398    }
4399
4400    #[test]
4401    fn test_normalize_field_spacing_no_space() {
4402        // Field has no space after colon
4403        let input = "Field:value\n";
4404        let deb822 = input.parse::<Deb822>().unwrap();
4405        let mut para = deb822.paragraphs().next().unwrap();
4406
4407        para.normalize_field_spacing();
4408        assert_eq!(para.to_string(), "Field: value\n");
4409    }
4410
4411    #[test]
4412    fn test_normalize_field_spacing_multiple_fields() {
4413        // Multiple fields with various spacing
4414        let input = "Field1:    value1\nField2:value2\nField3:  value3\n";
4415        let deb822 = input.parse::<Deb822>().unwrap();
4416        let mut para = deb822.paragraphs().next().unwrap();
4417
4418        para.normalize_field_spacing();
4419        assert_eq!(
4420            para.to_string(),
4421            "Field1: value1\nField2: value2\nField3: value3\n"
4422        );
4423    }
4424
4425    #[test]
4426    fn test_normalize_field_spacing_multiline_value() {
4427        // Field with multiline value
4428        let input = "Description:    short\n continuation line\n .  \n final line\n";
4429        let deb822 = input.parse::<Deb822>().unwrap();
4430        let mut para = deb822.paragraphs().next().unwrap();
4431
4432        para.normalize_field_spacing();
4433        assert_eq!(
4434            para.to_string(),
4435            "Description: short\n continuation line\n .  \n final line\n"
4436        );
4437    }
4438
4439    #[test]
4440    fn test_normalize_field_spacing_empty_value_with_whitespace() {
4441        // Field with empty value (only whitespace) should normalize to no space
4442        let input = "Field:  \n";
4443        let deb822 = input.parse::<Deb822>().unwrap();
4444        let mut para = deb822.paragraphs().next().unwrap();
4445
4446        para.normalize_field_spacing();
4447        // When value is empty/whitespace-only, normalize to no space
4448        assert_eq!(para.to_string(), "Field:\n");
4449    }
4450
4451    #[test]
4452    fn test_normalize_field_spacing_no_value() {
4453        // Field with no value (just newline) should stay unchanged
4454        let input = "Depends:\n";
4455        let deb822 = input.parse::<Deb822>().unwrap();
4456        let mut para = deb822.paragraphs().next().unwrap();
4457
4458        para.normalize_field_spacing();
4459        // Should remain with no space
4460        assert_eq!(para.to_string(), "Depends:\n");
4461    }
4462
4463    #[test]
4464    fn test_normalize_field_spacing_multiple_paragraphs() {
4465        // Multiple paragraphs
4466        let input = "Field1:    value1\n\nField2:  value2\n";
4467        let mut deb822 = input.parse::<Deb822>().unwrap();
4468
4469        deb822.normalize_field_spacing();
4470        assert_eq!(deb822.to_string(), "Field1: value1\n\nField2: value2\n");
4471    }
4472
4473    #[test]
4474    fn test_normalize_field_spacing_preserves_comments() {
4475        // Normalize spacing while preserving comments (comments are at document level)
4476        let input = "# Comment\nField:    value\n";
4477        let mut deb822 = input.parse::<Deb822>().unwrap();
4478
4479        deb822.normalize_field_spacing();
4480        assert_eq!(deb822.to_string(), "# Comment\nField: value\n");
4481    }
4482
4483    #[test]
4484    fn test_normalize_field_spacing_preserves_values() {
4485        // Ensure values are preserved exactly
4486        let input = "Source:   foo-bar\nMaintainer:Foo Bar <test@example.com>\n";
4487        let deb822 = input.parse::<Deb822>().unwrap();
4488        let mut para = deb822.paragraphs().next().unwrap();
4489
4490        para.normalize_field_spacing();
4491
4492        assert_eq!(para.get("Source").as_deref(), Some("foo-bar"));
4493        assert_eq!(
4494            para.get("Maintainer").as_deref(),
4495            Some("Foo Bar <test@example.com>")
4496        );
4497    }
4498
4499    #[test]
4500    fn test_normalize_field_spacing_tab_after_colon() {
4501        // Field with tab after colon (should be normalized to single space)
4502        let input = "Field:\tvalue\n";
4503        let deb822 = input.parse::<Deb822>().unwrap();
4504        let mut para = deb822.paragraphs().next().unwrap();
4505
4506        para.normalize_field_spacing();
4507        assert_eq!(para.to_string(), "Field: value\n");
4508    }
4509
4510    #[test]
4511    fn test_set_preserves_indentation() {
4512        // Test that Paragraph.set() preserves the original indentation
4513        let original = r#"Source: example
4514Build-Depends: foo,
4515               bar,
4516               baz
4517"#;
4518
4519        let mut para: super::Paragraph = original.parse().unwrap();
4520
4521        // Modify the Build-Depends field
4522        para.set("Build-Depends", "foo,\nbar,\nbaz");
4523
4524        // The indentation should be preserved (15 spaces for "Build-Depends: ")
4525        let expected = r#"Source: example
4526Build-Depends: foo,
4527               bar,
4528               baz
4529"#;
4530        assert_eq!(para.to_string(), expected);
4531    }
4532
4533    #[test]
4534    fn test_set_new_field_detects_field_name_length_indent() {
4535        // Test that new fields detect field-name-length-based indentation
4536        let original = r#"Source: example
4537Build-Depends: foo,
4538               bar,
4539               baz
4540Depends: lib1,
4541         lib2
4542"#;
4543
4544        let mut para: super::Paragraph = original.parse().unwrap();
4545
4546        // Add a new multi-line field - should detect that indentation is field-name-length + 2
4547        para.set("Recommends", "pkg1,\npkg2,\npkg3");
4548
4549        // "Recommends: " is 12 characters, so indentation should be 12 spaces
4550        assert!(para
4551            .to_string()
4552            .contains("Recommends: pkg1,\n            pkg2,"));
4553    }
4554
4555    #[test]
4556    fn test_set_new_field_detects_fixed_indent() {
4557        // Test that new fields detect fixed indentation pattern
4558        let original = r#"Source: example
4559Build-Depends: foo,
4560     bar,
4561     baz
4562Depends: lib1,
4563     lib2
4564"#;
4565
4566        let mut para: super::Paragraph = original.parse().unwrap();
4567
4568        // Add a new multi-line field - should detect fixed 5-space indentation
4569        para.set("Recommends", "pkg1,\npkg2,\npkg3");
4570
4571        // Should use the same 5-space indentation
4572        assert!(para
4573            .to_string()
4574            .contains("Recommends: pkg1,\n     pkg2,\n     pkg3\n"));
4575    }
4576
4577    #[test]
4578    fn test_set_new_field_no_multiline_fields() {
4579        // Test that new fields use field-name-length when no existing multi-line fields
4580        let original = r#"Source: example
4581Maintainer: Test <test@example.com>
4582"#;
4583
4584        let mut para: super::Paragraph = original.parse().unwrap();
4585
4586        // Add a new multi-line field - should default to field name length + 2
4587        para.set("Depends", "foo,\nbar,\nbaz");
4588
4589        // "Depends: " is 9 characters, so indentation should be 9 spaces
4590        let expected = r#"Source: example
4591Maintainer: Test <test@example.com>
4592Depends: foo,
4593         bar,
4594         baz
4595"#;
4596        assert_eq!(para.to_string(), expected);
4597    }
4598
4599    #[test]
4600    fn test_set_new_field_mixed_indentation() {
4601        // Test that new fields fall back to field-name-length when pattern is inconsistent
4602        let original = r#"Source: example
4603Build-Depends: foo,
4604               bar
4605Depends: lib1,
4606     lib2
4607"#;
4608
4609        let mut para: super::Paragraph = original.parse().unwrap();
4610
4611        // Add a new multi-line field - mixed pattern, should fall back to field name length + 2
4612        para.set("Recommends", "pkg1,\npkg2");
4613
4614        // "Recommends: " is 12 characters
4615        assert!(para
4616            .to_string()
4617            .contains("Recommends: pkg1,\n            pkg2\n"));
4618    }
4619
4620    #[test]
4621    fn test_entry_with_indentation() {
4622        // Test Entry::with_indentation directly
4623        let entry = super::Entry::with_indentation("Test-Field", "value1\nvalue2\nvalue3", "    ");
4624
4625        assert_eq!(
4626            entry.to_string(),
4627            "Test-Field: value1\n    value2\n    value3\n"
4628        );
4629    }
4630
4631    #[test]
4632    fn test_set_with_indent_pattern_fixed() {
4633        // Test setting a field with explicit fixed indentation pattern
4634        let original = r#"Source: example
4635Maintainer: Test <test@example.com>
4636"#;
4637
4638        let mut para: super::Paragraph = original.parse().unwrap();
4639
4640        // Add a new multi-line field with fixed 4-space indentation
4641        para.set_with_indent_pattern(
4642            "Depends",
4643            "foo,\nbar,\nbaz",
4644            Some(&super::IndentPattern::Fixed(4)),
4645            None,
4646        );
4647
4648        // Should use the specified 4-space indentation
4649        let expected = r#"Source: example
4650Maintainer: Test <test@example.com>
4651Depends: foo,
4652    bar,
4653    baz
4654"#;
4655        assert_eq!(para.to_string(), expected);
4656    }
4657
4658    #[test]
4659    fn test_set_with_indent_pattern_field_name_length() {
4660        // Test setting a field with field-name-length indentation pattern
4661        let original = r#"Source: example
4662Maintainer: Test <test@example.com>
4663"#;
4664
4665        let mut para: super::Paragraph = original.parse().unwrap();
4666
4667        // Add a new multi-line field with field-name-length indentation
4668        para.set_with_indent_pattern(
4669            "Build-Depends",
4670            "libfoo,\nlibbar,\nlibbaz",
4671            Some(&super::IndentPattern::FieldNameLength),
4672            None,
4673        );
4674
4675        // "Build-Depends: " is 15 characters, so indentation should be 15 spaces
4676        let expected = r#"Source: example
4677Maintainer: Test <test@example.com>
4678Build-Depends: libfoo,
4679               libbar,
4680               libbaz
4681"#;
4682        assert_eq!(para.to_string(), expected);
4683    }
4684
4685    #[test]
4686    fn test_set_with_indent_pattern_override_auto_detection() {
4687        // Test that explicit default pattern overrides auto-detection for new fields
4688        let original = r#"Source: example
4689Build-Depends: foo,
4690               bar,
4691               baz
4692"#;
4693
4694        let mut para: super::Paragraph = original.parse().unwrap();
4695
4696        // Add a NEW field with fixed 2-space indentation, overriding the auto-detected pattern
4697        para.set_with_indent_pattern(
4698            "Depends",
4699            "lib1,\nlib2,\nlib3",
4700            Some(&super::IndentPattern::Fixed(2)),
4701            None,
4702        );
4703
4704        // Should use the specified 2-space indentation, not the auto-detected 15-space
4705        let expected = r#"Source: example
4706Build-Depends: foo,
4707               bar,
4708               baz
4709Depends: lib1,
4710  lib2,
4711  lib3
4712"#;
4713        assert_eq!(para.to_string(), expected);
4714    }
4715
4716    #[test]
4717    fn test_set_with_indent_pattern_none_auto_detects() {
4718        // Test that None pattern auto-detects from existing fields
4719        let original = r#"Source: example
4720Build-Depends: foo,
4721     bar,
4722     baz
4723"#;
4724
4725        let mut para: super::Paragraph = original.parse().unwrap();
4726
4727        // Add a field with None pattern - should auto-detect fixed 5-space
4728        para.set_with_indent_pattern("Depends", "lib1,\nlib2", None, None);
4729
4730        // Should auto-detect and use the 5-space indentation
4731        let expected = r#"Source: example
4732Build-Depends: foo,
4733     bar,
4734     baz
4735Depends: lib1,
4736     lib2
4737"#;
4738        assert_eq!(para.to_string(), expected);
4739    }
4740
4741    #[test]
4742    fn test_set_with_indent_pattern_with_field_order() {
4743        // Test setting a field with both indent pattern and field ordering
4744        let original = r#"Source: example
4745Maintainer: Test <test@example.com>
4746"#;
4747
4748        let mut para: super::Paragraph = original.parse().unwrap();
4749
4750        // Add a field with fixed indentation and specific field ordering
4751        para.set_with_indent_pattern(
4752            "Priority",
4753            "optional",
4754            Some(&super::IndentPattern::Fixed(4)),
4755            Some(&["Source", "Priority", "Maintainer"]),
4756        );
4757
4758        // Priority should be inserted between Source and Maintainer
4759        let expected = r#"Source: example
4760Priority: optional
4761Maintainer: Test <test@example.com>
4762"#;
4763        assert_eq!(para.to_string(), expected);
4764    }
4765
4766    #[test]
4767    fn test_set_with_indent_pattern_replace_existing() {
4768        // Test that replacing an existing multi-line field preserves its indentation
4769        let original = r#"Source: example
4770Depends: foo,
4771         bar
4772"#;
4773
4774        let mut para: super::Paragraph = original.parse().unwrap();
4775
4776        // Replace Depends - the default pattern is ignored, existing indentation is preserved
4777        para.set_with_indent_pattern(
4778            "Depends",
4779            "lib1,\nlib2,\nlib3",
4780            Some(&super::IndentPattern::Fixed(3)),
4781            None,
4782        );
4783
4784        // Should preserve the existing 9-space indentation, not use the default 3-space
4785        let expected = r#"Source: example
4786Depends: lib1,
4787         lib2,
4788         lib3
4789"#;
4790        assert_eq!(para.to_string(), expected);
4791    }
4792
4793    #[test]
4794    fn test_change_field_indent() {
4795        // Test changing indentation of an existing field without changing its value
4796        let original = r#"Source: example
4797Depends: foo,
4798         bar,
4799         baz
4800"#;
4801        let mut para: super::Paragraph = original.parse().unwrap();
4802
4803        // Change Depends field to use 2-space indentation
4804        let result = para
4805            .change_field_indent("Depends", &super::IndentPattern::Fixed(2))
4806            .unwrap();
4807        assert!(result, "Field should have been found and updated");
4808
4809        let expected = r#"Source: example
4810Depends: foo,
4811  bar,
4812  baz
4813"#;
4814        assert_eq!(para.to_string(), expected);
4815    }
4816
4817    #[test]
4818    fn test_change_field_indent_nonexistent() {
4819        // Test changing indentation of a non-existent field
4820        let original = r#"Source: example
4821"#;
4822        let mut para: super::Paragraph = original.parse().unwrap();
4823
4824        // Try to change indentation of non-existent field
4825        let result = para
4826            .change_field_indent("Depends", &super::IndentPattern::Fixed(2))
4827            .unwrap();
4828        assert!(!result, "Should return false for non-existent field");
4829
4830        // Paragraph should be unchanged
4831        assert_eq!(para.to_string(), original);
4832    }
4833
4834    #[test]
4835    fn test_change_field_indent_case_insensitive() {
4836        // Test that change_field_indent is case-insensitive
4837        let original = r#"Build-Depends: foo,
4838               bar
4839"#;
4840        let mut para: super::Paragraph = original.parse().unwrap();
4841
4842        // Change using different case
4843        let result = para
4844            .change_field_indent("build-depends", &super::IndentPattern::Fixed(1))
4845            .unwrap();
4846        assert!(result, "Should find field case-insensitively");
4847
4848        let expected = r#"Build-Depends: foo,
4849 bar
4850"#;
4851        assert_eq!(para.to_string(), expected);
4852    }
4853
4854    #[test]
4855    fn test_entry_get_indent() {
4856        // Test that we can extract indentation from an entry
4857        let original = r#"Build-Depends: foo,
4858               bar,
4859               baz
4860"#;
4861        let para: super::Paragraph = original.parse().unwrap();
4862        let entry = para.entries().next().unwrap();
4863
4864        assert_eq!(entry.get_indent(), Some("               ".to_string()));
4865    }
4866
4867    #[test]
4868    fn test_entry_get_indent_single_line() {
4869        // Single-line entries should return None for indentation
4870        let original = r#"Source: example
4871"#;
4872        let para: super::Paragraph = original.parse().unwrap();
4873        let entry = para.entries().next().unwrap();
4874
4875        assert_eq!(entry.get_indent(), None);
4876    }
4877}
4878
4879#[test]
4880fn test_move_paragraph_forward() {
4881    let mut d: Deb822 = vec![
4882        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4883        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4884        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4885    ]
4886    .into_iter()
4887    .collect();
4888    d.move_paragraph(0, 2);
4889    assert_eq!(
4890        d.to_string(),
4891        "A: B\nC: D\n\nX: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n"
4892    );
4893}
4894
4895#[test]
4896fn test_move_paragraph_backward() {
4897    let mut d: Deb822 = vec![
4898        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4899        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4900        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4901    ]
4902    .into_iter()
4903    .collect();
4904    d.move_paragraph(2, 0);
4905    assert_eq!(
4906        d.to_string(),
4907        "X: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n\nA: B\nC: D\n"
4908    );
4909}
4910
4911#[test]
4912fn test_move_paragraph_middle() {
4913    let mut d: Deb822 = vec![
4914        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4915        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4916        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4917    ]
4918    .into_iter()
4919    .collect();
4920    d.move_paragraph(2, 1);
4921    assert_eq!(
4922        d.to_string(),
4923        "Foo: Bar\nBaz: Qux\n\nX: Y\nZ: W\n\nA: B\nC: D\n"
4924    );
4925}
4926
4927#[test]
4928fn test_move_paragraph_same_index() {
4929    let mut d: Deb822 = vec![
4930        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4931        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4932    ]
4933    .into_iter()
4934    .collect();
4935    let original = d.to_string();
4936    d.move_paragraph(1, 1);
4937    assert_eq!(d.to_string(), original);
4938}
4939
4940#[test]
4941fn test_move_paragraph_single() {
4942    let mut d: Deb822 = vec![vec![("Foo", "Bar")].into_iter().collect()]
4943        .into_iter()
4944        .collect();
4945    let original = d.to_string();
4946    d.move_paragraph(0, 0);
4947    assert_eq!(d.to_string(), original);
4948}
4949
4950#[test]
4951fn test_move_paragraph_invalid_index() {
4952    let mut d: Deb822 = vec![
4953        vec![("Foo", "Bar")].into_iter().collect(),
4954        vec![("A", "B")].into_iter().collect(),
4955    ]
4956    .into_iter()
4957    .collect();
4958    let original = d.to_string();
4959    d.move_paragraph(0, 5);
4960    assert_eq!(d.to_string(), original);
4961}
4962
4963#[test]
4964fn test_move_paragraph_with_comments() {
4965    let text = r#"Foo: Bar
4966
4967# This is a comment
4968
4969A: B
4970
4971X: Y
4972"#;
4973    let mut d: Deb822 = text.parse().unwrap();
4974    d.move_paragraph(0, 2);
4975    assert_eq!(
4976        d.to_string(),
4977        "# This is a comment\n\nA: B\n\nX: Y\n\nFoo: Bar\n"
4978    );
4979}
4980
4981#[test]
4982fn test_case_insensitive_get() {
4983    let text = "Package: test\nVersion: 1.0\n";
4984    let d: Deb822 = text.parse().unwrap();
4985    let p = d.paragraphs().next().unwrap();
4986
4987    // Test different case variations
4988    assert_eq!(p.get("Package").as_deref(), Some("test"));
4989    assert_eq!(p.get("package").as_deref(), Some("test"));
4990    assert_eq!(p.get("PACKAGE").as_deref(), Some("test"));
4991    assert_eq!(p.get("PaCkAgE").as_deref(), Some("test"));
4992
4993    assert_eq!(p.get("Version").as_deref(), Some("1.0"));
4994    assert_eq!(p.get("version").as_deref(), Some("1.0"));
4995    assert_eq!(p.get("VERSION").as_deref(), Some("1.0"));
4996}
4997
4998#[test]
4999fn test_case_insensitive_set() {
5000    let text = "Package: test\n";
5001    let d: Deb822 = text.parse().unwrap();
5002    let mut p = d.paragraphs().next().unwrap();
5003
5004    // Set with different case should update the existing field
5005    p.set("package", "updated");
5006    assert_eq!(p.get("Package").as_deref(), Some("updated"));
5007    assert_eq!(p.get("package").as_deref(), Some("updated"));
5008
5009    // Set with UPPERCASE
5010    p.set("PACKAGE", "updated2");
5011    assert_eq!(p.get("Package").as_deref(), Some("updated2"));
5012
5013    // Field count should remain 1
5014    assert_eq!(p.keys().count(), 1);
5015}
5016
5017#[test]
5018fn test_case_insensitive_remove() {
5019    let text = "Package: test\nVersion: 1.0\n";
5020    let d: Deb822 = text.parse().unwrap();
5021    let mut p = d.paragraphs().next().unwrap();
5022
5023    // Remove with different case
5024    p.remove("package");
5025    assert_eq!(p.get("Package"), None);
5026    assert_eq!(p.get("Version").as_deref(), Some("1.0"));
5027
5028    // Remove with uppercase
5029    p.remove("VERSION");
5030    assert_eq!(p.get("Version"), None);
5031
5032    // No fields left
5033    assert_eq!(p.keys().count(), 0);
5034}
5035
5036#[test]
5037fn test_case_preservation() {
5038    let text = "Package: test\n";
5039    let d: Deb822 = text.parse().unwrap();
5040    let mut p = d.paragraphs().next().unwrap();
5041
5042    // Original case should be preserved
5043    let original_text = d.to_string();
5044    assert_eq!(original_text, "Package: test\n");
5045
5046    // Set with different case should preserve original case
5047    p.set("package", "updated");
5048
5049    // The field name should still be "Package" (original case preserved)
5050    let updated_text = d.to_string();
5051    assert_eq!(updated_text, "Package: updated\n");
5052}
5053
5054#[test]
5055fn test_case_insensitive_contains_key() {
5056    let text = "Package: test\n";
5057    let d: Deb822 = text.parse().unwrap();
5058    let p = d.paragraphs().next().unwrap();
5059
5060    assert!(p.contains_key("Package"));
5061    assert!(p.contains_key("package"));
5062    assert!(p.contains_key("PACKAGE"));
5063    assert!(!p.contains_key("NonExistent"));
5064}
5065
5066#[test]
5067fn test_case_insensitive_get_all() {
5068    let text = "Package: test1\npackage: test2\n";
5069    let d: Deb822 = text.parse().unwrap();
5070    let p = d.paragraphs().next().unwrap();
5071
5072    let values: Vec<String> = p.get_all("PACKAGE").collect();
5073    assert_eq!(values, vec!["test1", "test2"]);
5074}
5075
5076#[test]
5077fn test_case_insensitive_rename() {
5078    let text = "Package: test\n";
5079    let d: Deb822 = text.parse().unwrap();
5080    let mut p = d.paragraphs().next().unwrap();
5081
5082    // Rename with different case
5083    assert!(p.rename("package", "NewName"));
5084    assert_eq!(p.get("NewName").as_deref(), Some("test"));
5085    assert_eq!(p.get("Package"), None);
5086}
5087
5088#[test]
5089fn test_rename_changes_case() {
5090    let text = "Package: test\n";
5091    let d: Deb822 = text.parse().unwrap();
5092    let mut p = d.paragraphs().next().unwrap();
5093
5094    // Rename to different case of the same name
5095    assert!(p.rename("package", "PACKAGE"));
5096
5097    // The field name should now be uppercase
5098    let updated_text = d.to_string();
5099    assert_eq!(updated_text, "PACKAGE: test\n");
5100
5101    // Can still get with any case
5102    assert_eq!(p.get("package").as_deref(), Some("test"));
5103    assert_eq!(p.get("Package").as_deref(), Some("test"));
5104    assert_eq!(p.get("PACKAGE").as_deref(), Some("test"));
5105}
5106
5107#[test]
5108fn test_rename_preserves_indentation_and_whitespace() {
5109    // When renaming a field, the original post-colon whitespace and
5110    // continuation-line indentation must be preserved (only the key changes).
5111    let text =
5112        "Comments:     Exceptions\n            1997-1999, 2003 MIT\n            License terms\n";
5113    let d: Deb822 = text.parse().unwrap();
5114    let mut p = d.paragraphs().next().unwrap();
5115
5116    assert!(p.rename("Comments", "Comment"));
5117    assert_eq!(
5118        d.to_string(),
5119        "Comment:     Exceptions\n            1997-1999, 2003 MIT\n            License terms\n"
5120    );
5121}
5122
5123#[test]
5124fn test_rename_in_multi_field_paragraph() {
5125    // Reproduce the intel-mkl scenario: Comments is the last of several
5126    // fields, with multi-line value containing internal indentation.
5127    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";
5128    let d: Deb822 = text.parse().unwrap();
5129    let mut p = d.paragraphs().next().unwrap();
5130
5131    assert!(p.rename("Comments", "Comment"));
5132    assert_eq!(d.to_string(), text.replace("Comments:", "Comment:"));
5133}
5134
5135#[test]
5136fn test_rename_preserves_post_colon_whitespace() {
5137    // A single-line value with non-default post-colon whitespace must keep it.
5138    let text = "Files:     install_GUI.sh\n";
5139    let d: Deb822 = text.parse().unwrap();
5140    let mut p = d.paragraphs().next().unwrap();
5141
5142    assert!(p.rename("Files", "File"));
5143    assert_eq!(d.to_string(), "File:     install_GUI.sh\n");
5144}
5145
5146#[test]
5147fn test_reject_whitespace_only_continuation_line() {
5148    // Issue #350: A continuation line with only whitespace should not be accepted
5149    // According to Debian Policy, continuation lines must have content after the leading space
5150    // A line with only whitespace (like " \n") should terminate the field
5151
5152    // This should be rejected/treated as an error
5153    let text = "Build-Depends:\n \ndebhelper\n";
5154    let parsed = Deb822::parse(text);
5155
5156    // The empty line with just whitespace should cause an error
5157    // or at minimum, should not be included as part of the field value
5158    assert!(
5159        !parsed.errors().is_empty(),
5160        "Expected parse errors for whitespace-only continuation line"
5161    );
5162}
5163
5164#[test]
5165fn test_reject_empty_continuation_line_in_multiline_field() {
5166    // Test that an empty line terminates a multi-line field (and generates an error)
5167    let text = "Depends: foo,\n bar,\n \n baz\n";
5168    let parsed = Deb822::parse(text);
5169
5170    // The empty line should cause parse errors
5171    assert!(
5172        !parsed.errors().is_empty(),
5173        "Empty continuation line should generate parse errors"
5174    );
5175
5176    // Verify we got the specific error about empty continuation line
5177    let has_empty_line_error = parsed
5178        .errors()
5179        .iter()
5180        .any(|e| e.contains("empty continuation line"));
5181    assert!(
5182        has_empty_line_error,
5183        "Should have an error about empty continuation line"
5184    );
5185}
5186
5187#[test]
5188#[should_panic(expected = "empty continuation line")]
5189fn test_set_rejects_empty_continuation_lines() {
5190    // Test that Paragraph.set() panics for values with empty continuation lines
5191    let text = "Package: test\n";
5192    let deb822 = text.parse::<Deb822>().unwrap();
5193    let mut para = deb822.paragraphs().next().unwrap();
5194
5195    // Try to set a field with an empty continuation line
5196    // This should panic with an appropriate error message
5197    let value_with_empty_line = "foo\n \nbar";
5198    para.set("Depends", value_with_empty_line);
5199}
5200
5201#[test]
5202fn test_try_set_returns_error_for_empty_continuation_lines() {
5203    // Test that Paragraph.try_set() returns an error for values with empty continuation lines
5204    let text = "Package: test\n";
5205    let deb822 = text.parse::<Deb822>().unwrap();
5206    let mut para = deb822.paragraphs().next().unwrap();
5207
5208    // Try to set a field with an empty continuation line
5209    let value_with_empty_line = "foo\n \nbar";
5210    let result = para.try_set("Depends", value_with_empty_line);
5211
5212    // Should return an error
5213    assert!(
5214        result.is_err(),
5215        "try_set() should return an error for empty continuation lines"
5216    );
5217
5218    // Verify it's the right kind of error
5219    match result {
5220        Err(Error::InvalidValue(msg)) => {
5221            assert!(
5222                msg.contains("empty continuation line"),
5223                "Error message should mention empty continuation line"
5224            );
5225        }
5226        _ => panic!("Expected InvalidValue error"),
5227    }
5228}
5229
5230#[test]
5231fn test_try_set_with_indent_pattern_returns_error() {
5232    // Test that try_set_with_indent_pattern() returns an error for empty continuation lines
5233    let text = "Package: test\n";
5234    let deb822 = text.parse::<Deb822>().unwrap();
5235    let mut para = deb822.paragraphs().next().unwrap();
5236
5237    let value_with_empty_line = "foo\n \nbar";
5238    let result = para.try_set_with_indent_pattern(
5239        "Depends",
5240        value_with_empty_line,
5241        Some(&IndentPattern::Fixed(2)),
5242        None,
5243    );
5244
5245    assert!(
5246        result.is_err(),
5247        "try_set_with_indent_pattern() should return an error"
5248    );
5249}
5250
5251#[test]
5252fn test_try_set_succeeds_for_valid_value() {
5253    // Test that try_set() succeeds for valid values
5254    let text = "Package: test\n";
5255    let deb822 = text.parse::<Deb822>().unwrap();
5256    let mut para = deb822.paragraphs().next().unwrap();
5257
5258    // Valid multiline value
5259    let valid_value = "foo\nbar";
5260    let result = para.try_set("Depends", valid_value);
5261
5262    assert!(result.is_ok(), "try_set() should succeed for valid values");
5263    assert_eq!(para.get("Depends").as_deref(), Some("foo\nbar"));
5264}
5265
5266#[test]
5267fn test_field_with_empty_first_line() {
5268    // Test parsing a field where the value starts on a continuation line (empty first line)
5269    // This is valid according to Debian Policy - the first line can be empty
5270    let text = "Foo:\n blah\n blah\n";
5271    let parsed = Deb822::parse(text);
5272
5273    // This should be valid - no errors
5274    assert!(
5275        parsed.errors().is_empty(),
5276        "Empty first line should be valid. Got errors: {:?}",
5277        parsed.errors()
5278    );
5279
5280    let deb822 = parsed.tree();
5281    let para = deb822.paragraphs().next().unwrap();
5282    assert_eq!(para.get("Foo").as_deref(), Some("blah\nblah"));
5283}
5284
5285#[test]
5286fn test_try_set_with_empty_first_line() {
5287    // Test that try_set() works with values that have empty first line
5288    let text = "Package: test\n";
5289    let deb822 = text.parse::<Deb822>().unwrap();
5290    let mut para = deb822.paragraphs().next().unwrap();
5291
5292    // Value with empty first line - this should be valid
5293    let value = "\nblah\nmore";
5294    let result = para.try_set("Depends", value);
5295
5296    assert!(
5297        result.is_ok(),
5298        "try_set() should succeed for values with empty first line. Got: {:?}",
5299        result
5300    );
5301}
5302
5303#[test]
5304fn test_field_with_value_then_empty_continuation() {
5305    // Test that a field with a value on the first line followed by empty continuation is rejected
5306    let text = "Foo: bar\n \n";
5307    let parsed = Deb822::parse(text);
5308
5309    // This should have errors - empty continuation line after initial value
5310    assert!(
5311        !parsed.errors().is_empty(),
5312        "Field with value then empty continuation line should be rejected"
5313    );
5314
5315    // Verify we got the specific error about empty continuation line
5316    let has_empty_line_error = parsed
5317        .errors()
5318        .iter()
5319        .any(|e| e.contains("empty continuation line"));
5320    assert!(
5321        has_empty_line_error,
5322        "Should have error about empty continuation line"
5323    );
5324}
5325
5326#[test]
5327fn test_substvar_continuation_line() {
5328    let text = "\
5329Package: python3-cryptography
5330Architecture: any
5331Depends: python3-bcrypt,
5332         ${misc:Depends},
5333         ${python3:Depends},
5334         ${shlibs:Depends},
5335Suggests: python-cryptography-doc,
5336          python3-cryptography-vectors,
5337Description: Python library exposing cryptographic recipes and primitives
5338 The cryptography library is designed to be a \"one-stop-shop\" for
5339 all your cryptographic needs in Python.
5340 .
5341 As an alternative to the libraries that came before it, cryptography
5342 tries to address some of the issues with those libraries:
5343  - Lack of PyPy and Python 3 support.
5344  - Lack of maintenance.
5345  - Use of poor implementations of algorithms (i.e. ones with known
5346    side-channel attacks).
5347  - Lack of high level, \"Cryptography for humans\", APIs.
5348  - Absence of algorithms such as AES-GCM.
5349  - Poor introspectability, and thus poor testability.
5350  - Extremely error prone APIs, and bad defaults.
5351";
5352    let parsed = Deb822::parse(text);
5353    for e in parsed.positioned_errors() {
5354        eprintln!("error at {:?}: {}", e.range, e.message);
5355    }
5356    assert!(
5357        parsed.errors().is_empty(),
5358        "Should not produce errors: {:?}",
5359        parsed.errors()
5360    );
5361    assert!(
5362        parsed.positioned_errors().is_empty(),
5363        "Should not produce positioned errors: {:?}",
5364        parsed.positioned_errors()
5365    );
5366}
5367
5368#[test]
5369fn test_line_col() {
5370    let text = r#"Source: foo
5371Maintainer: Foo Bar <jelmer@jelmer.uk>
5372Section: net
5373
5374Package: foo
5375Architecture: all
5376Depends: libc6
5377Description: This is a description
5378 With details
5379"#;
5380    let deb822 = text.parse::<Deb822>().unwrap();
5381
5382    // Test paragraph line numbers
5383    let paras: Vec<_> = deb822.paragraphs().collect();
5384    assert_eq!(paras.len(), 2);
5385
5386    // First paragraph starts at line 0
5387    assert_eq!(paras[0].line(), 0);
5388    assert_eq!(paras[0].column(), 0);
5389
5390    // Second paragraph starts at line 4 (after the empty line)
5391    assert_eq!(paras[1].line(), 4);
5392    assert_eq!(paras[1].column(), 0);
5393
5394    // Test entry line numbers
5395    let entries: Vec<_> = paras[0].entries().collect();
5396    assert_eq!(entries[0].line(), 0); // Source: foo
5397    assert_eq!(entries[1].line(), 1); // Maintainer: ...
5398    assert_eq!(entries[2].line(), 2); // Section: net
5399
5400    // Test column numbers
5401    assert_eq!(entries[0].column(), 0); // Start of line
5402    assert_eq!(entries[1].column(), 0); // Start of line
5403
5404    // Test line_col() method
5405    assert_eq!(paras[1].line_col(), (4, 0));
5406    assert_eq!(entries[0].line_col(), (0, 0));
5407
5408    // Test multi-line entry
5409    let second_para_entries: Vec<_> = paras[1].entries().collect();
5410    assert_eq!(second_para_entries[3].line(), 7); // Description starts at line 7
5411}
5412
5413#[test]
5414fn test_deb822_snapshot_independence() {
5415    let text = r#"Source: foo
5416Maintainer: Joe <joe@example.com>
5417
5418Package: foo
5419Architecture: all
5420"#;
5421    let deb822 = text.parse::<Deb822>().unwrap();
5422    let snap = deb822.snapshot();
5423    assert!(deb822.tree_eq(&snap));
5424
5425    let mut para = deb822.paragraphs().next().unwrap();
5426    para.set("Source", "modified");
5427
5428    // snapshot unchanged
5429    let snap_para = snap.paragraphs().next().unwrap();
5430    assert_eq!(snap_para.get("Source").as_deref(), Some("foo"));
5431    // ... and now they have diverged
5432    assert!(!deb822.tree_eq(&snap));
5433}
5434
5435#[test]
5436fn test_paragraph_snapshot_independence() {
5437    let text = "Package: foo\nArchitecture: all\n";
5438    let deb822 = text.parse::<Deb822>().unwrap();
5439    let mut para = deb822.paragraphs().next().unwrap();
5440    let snap = para.snapshot();
5441    assert!(para.tree_eq(&snap));
5442
5443    para.set("Package", "modified");
5444    assert_eq!(snap.get("Package").as_deref(), Some("foo"));
5445    assert!(!para.tree_eq(&snap));
5446}
5447
5448#[test]
5449fn test_tree_eq_value_equivalence() {
5450    // Two independently-parsed trees with identical content should be tree_eq
5451    // (no shared green pointer, but structural equality holds).
5452    let text = "Package: foo\nArchitecture: all\n";
5453    let a = text.parse::<Deb822>().unwrap();
5454    let b = text.parse::<Deb822>().unwrap();
5455    assert!(a.tree_eq(&b));
5456    assert!(b.tree_eq(&a));
5457
5458    // Different content => not tree_eq.
5459    let c: Deb822 = "Package: bar\n".parse().unwrap();
5460    assert!(!a.tree_eq(&c));
5461}
5462
5463#[test]
5464fn test_entry_snapshot_independence() {
5465    let text = "Package: foo\n";
5466    let deb822 = text.parse::<Deb822>().unwrap();
5467    let mut para = deb822.paragraphs().next().unwrap();
5468    let entry = para.entries().next().unwrap();
5469    let snap = entry.snapshot();
5470    assert!(entry.tree_eq(&snap));
5471
5472    para.set("Package", "modified");
5473    // The snapshot entry points to an independent tree
5474    assert_eq!(snap.value(), "foo");
5475}
5476
5477#[test]
5478fn test_paragraph_text_range() {
5479    // Test that text_range() returns the correct range for a paragraph
5480    let text = r#"Source: foo
5481Maintainer: Joe <joe@example.com>
5482
5483Package: foo
5484Architecture: all
5485"#;
5486    let deb822 = text.parse::<Deb822>().unwrap();
5487    let paras: Vec<_> = deb822.paragraphs().collect();
5488
5489    // First paragraph
5490    let range1 = paras[0].text_range();
5491    let para1_text = &text[range1.start().into()..range1.end().into()];
5492    assert_eq!(
5493        para1_text,
5494        "Source: foo\nMaintainer: Joe <joe@example.com>\n"
5495    );
5496
5497    // Second paragraph
5498    let range2 = paras[1].text_range();
5499    let para2_text = &text[range2.start().into()..range2.end().into()];
5500    assert_eq!(para2_text, "Package: foo\nArchitecture: all\n");
5501}
5502
5503#[test]
5504fn test_paragraphs_in_range_single() {
5505    // Test finding a single paragraph in range
5506    let text = r#"Source: foo
5507
5508Package: bar
5509
5510Package: baz
5511"#;
5512    let deb822 = text.parse::<Deb822>().unwrap();
5513
5514    // Get range of first paragraph
5515    let first_para = deb822.paragraphs().next().unwrap();
5516    let range = first_para.text_range();
5517
5518    // Query paragraphs in that range
5519    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5520    assert_eq!(paras.len(), 1);
5521    assert_eq!(paras[0].get("Source").as_deref(), Some("foo"));
5522}
5523
5524#[test]
5525fn test_paragraphs_in_range_multiple() {
5526    // Test finding multiple paragraphs in range
5527    let text = r#"Source: foo
5528
5529Package: bar
5530
5531Package: baz
5532"#;
5533    let deb822 = text.parse::<Deb822>().unwrap();
5534
5535    // Create a range that covers first two paragraphs
5536    let range = rowan::TextRange::new(0.into(), 25.into());
5537
5538    // Query paragraphs in that range
5539    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5540    assert_eq!(paras.len(), 2);
5541    assert_eq!(paras[0].get("Source").as_deref(), Some("foo"));
5542    assert_eq!(paras[1].get("Package").as_deref(), Some("bar"));
5543}
5544
5545#[test]
5546fn test_paragraphs_in_range_partial_overlap() {
5547    // Test that paragraphs are included if they partially overlap with the range
5548    let text = r#"Source: foo
5549
5550Package: bar
5551
5552Package: baz
5553"#;
5554    let deb822 = text.parse::<Deb822>().unwrap();
5555
5556    // Create a range that starts in the middle of the second paragraph
5557    let range = rowan::TextRange::new(15.into(), 30.into());
5558
5559    // Should include the second paragraph since it overlaps
5560    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5561    assert!(paras.len() >= 1);
5562    assert!(paras
5563        .iter()
5564        .any(|p| p.get("Package").as_deref() == Some("bar")));
5565}
5566
5567#[test]
5568fn test_paragraphs_in_range_no_match() {
5569    // Test that empty iterator is returned when no paragraphs are in range
5570    let text = r#"Source: foo
5571
5572Package: bar
5573"#;
5574    let deb822 = text.parse::<Deb822>().unwrap();
5575
5576    // Create a range that's way beyond the document
5577    let range = rowan::TextRange::new(1000.into(), 2000.into());
5578
5579    // Should return empty iterator
5580    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5581    assert_eq!(paras.len(), 0);
5582}
5583
5584#[test]
5585fn test_paragraphs_in_range_all() {
5586    // Test finding all paragraphs when range covers entire document
5587    let text = r#"Source: foo
5588
5589Package: bar
5590
5591Package: baz
5592"#;
5593    let deb822 = text.parse::<Deb822>().unwrap();
5594
5595    // Create a range that covers the entire document
5596    let range = rowan::TextRange::new(0.into(), text.len().try_into().unwrap());
5597
5598    // Should return all paragraphs
5599    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5600    assert_eq!(paras.len(), 3);
5601}
5602
5603#[test]
5604fn test_paragraph_at_position() {
5605    // Test finding paragraph at a given text offset
5606    let text = r#"Package: foo
5607Version: 1.0
5608
5609Package: bar
5610Architecture: all
5611"#;
5612    let deb822 = text.parse::<Deb822>().unwrap();
5613
5614    // Position 5 is within first paragraph ("Package: foo")
5615    let para = deb822.paragraph_at_position(rowan::TextSize::from(5));
5616    assert!(para.is_some());
5617    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5618
5619    // Position 30 is within second paragraph
5620    let para = deb822.paragraph_at_position(rowan::TextSize::from(30));
5621    assert!(para.is_some());
5622    assert_eq!(para.unwrap().get("Package").as_deref(), Some("bar"));
5623
5624    // Position beyond document
5625    let para = deb822.paragraph_at_position(rowan::TextSize::from(1000));
5626    assert!(para.is_none());
5627}
5628
5629#[test]
5630fn test_paragraph_at_line() {
5631    // Test finding paragraph at a given line number
5632    let text = r#"Package: foo
5633Version: 1.0
5634
5635Package: bar
5636Architecture: all
5637"#;
5638    let deb822 = text.parse::<Deb822>().unwrap();
5639
5640    // Line 0 is in first paragraph
5641    let para = deb822.paragraph_at_line(0);
5642    assert!(para.is_some());
5643    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5644
5645    // Line 1 is also in first paragraph
5646    let para = deb822.paragraph_at_line(1);
5647    assert!(para.is_some());
5648    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5649
5650    // Line 3 is in second paragraph
5651    let para = deb822.paragraph_at_line(3);
5652    assert!(para.is_some());
5653    assert_eq!(para.unwrap().get("Package").as_deref(), Some("bar"));
5654
5655    // Line beyond document
5656    let para = deb822.paragraph_at_line(100);
5657    assert!(para.is_none());
5658}
5659
5660#[test]
5661fn test_entry_at_line_col() {
5662    // Test finding entry at a given line/column position
5663    let text = r#"Package: foo
5664Version: 1.0
5665Architecture: all
5666"#;
5667    let deb822 = text.parse::<Deb822>().unwrap();
5668
5669    // Line 0, column 0 is in "Package: foo"
5670    let entry = deb822.entry_at_line_col(0, 0);
5671    assert!(entry.is_some());
5672    assert_eq!(entry.unwrap().key(), Some("Package".to_string()));
5673
5674    // Line 1, column 0 is in "Version: 1.0"
5675    let entry = deb822.entry_at_line_col(1, 0);
5676    assert!(entry.is_some());
5677    assert_eq!(entry.unwrap().key(), Some("Version".to_string()));
5678
5679    // Line 2, column 5 is in "Architecture: all"
5680    let entry = deb822.entry_at_line_col(2, 5);
5681    assert!(entry.is_some());
5682    assert_eq!(entry.unwrap().key(), Some("Architecture".to_string()));
5683
5684    // Position beyond document
5685    let entry = deb822.entry_at_line_col(100, 0);
5686    assert!(entry.is_none());
5687}
5688
5689#[test]
5690fn test_entry_at_line_col_multiline() {
5691    // Test finding entry in a multiline value
5692    let text = r#"Package: foo
5693Description: A package
5694 with a long
5695 description
5696Version: 1.0
5697"#;
5698    let deb822 = text.parse::<Deb822>().unwrap();
5699
5700    // Line 1 is the start of Description
5701    let entry = deb822.entry_at_line_col(1, 0);
5702    assert!(entry.is_some());
5703    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5704
5705    // Line 2 is continuation of Description
5706    let entry = deb822.entry_at_line_col(2, 1);
5707    assert!(entry.is_some());
5708    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5709
5710    // Line 3 is also continuation of Description
5711    let entry = deb822.entry_at_line_col(3, 1);
5712    assert!(entry.is_some());
5713    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5714
5715    // Line 4 is Version
5716    let entry = deb822.entry_at_line_col(4, 0);
5717    assert!(entry.is_some());
5718    assert_eq!(entry.unwrap().key(), Some("Version".to_string()));
5719}
5720
5721#[test]
5722fn test_entries_in_range() {
5723    // Test finding entries in a paragraph within a range
5724    let text = r#"Package: foo
5725Version: 1.0
5726Architecture: all
5727"#;
5728    let deb822 = text.parse::<Deb822>().unwrap();
5729    let para = deb822.paragraphs().next().unwrap();
5730
5731    // Get first entry's range
5732    let first_entry = para.entries().next().unwrap();
5733    let range = first_entry.text_range();
5734
5735    // Query entries in that range - should get only first entry
5736    let entries: Vec<_> = para.entries_in_range(range).collect();
5737    assert_eq!(entries.len(), 1);
5738    assert_eq!(entries[0].key(), Some("Package".to_string()));
5739
5740    // Query with a range covering first two entries
5741    let range = rowan::TextRange::new(0.into(), 25.into());
5742    let entries: Vec<_> = para.entries_in_range(range).collect();
5743    assert_eq!(entries.len(), 2);
5744    assert_eq!(entries[0].key(), Some("Package".to_string()));
5745    assert_eq!(entries[1].key(), Some("Version".to_string()));
5746}
5747
5748#[test]
5749fn test_entries_in_range_partial_overlap() {
5750    // Test that entries with partial overlap are included
5751    let text = r#"Package: foo
5752Version: 1.0
5753Architecture: all
5754"#;
5755    let deb822 = text.parse::<Deb822>().unwrap();
5756    let para = deb822.paragraphs().next().unwrap();
5757
5758    // Create a range that starts in the middle of the second entry
5759    let range = rowan::TextRange::new(15.into(), 30.into());
5760
5761    let entries: Vec<_> = para.entries_in_range(range).collect();
5762    assert!(entries.len() >= 1);
5763    assert!(entries
5764        .iter()
5765        .any(|e| e.key() == Some("Version".to_string())));
5766}
5767
5768#[test]
5769fn test_entries_in_range_no_match() {
5770    // Test that empty iterator is returned when no entries match
5771    let text = "Package: foo\n";
5772    let deb822 = text.parse::<Deb822>().unwrap();
5773    let para = deb822.paragraphs().next().unwrap();
5774
5775    // Range beyond the paragraph
5776    let range = rowan::TextRange::new(1000.into(), 2000.into());
5777    let entries: Vec<_> = para.entries_in_range(range).collect();
5778    assert_eq!(entries.len(), 0);
5779}
5780
5781#[test]
5782fn test_entry_at_position() {
5783    // Test finding entry at a specific text offset
5784    let text = r#"Package: foo
5785Version: 1.0
5786Architecture: all
5787"#;
5788    let deb822 = text.parse::<Deb822>().unwrap();
5789    let para = deb822.paragraphs().next().unwrap();
5790
5791    // Position 5 is within "Package: foo"
5792    let entry = para.entry_at_position(rowan::TextSize::from(5));
5793    assert!(entry.is_some());
5794    assert_eq!(entry.unwrap().key(), Some("Package".to_string()));
5795
5796    // Position 15 is within "Version: 1.0"
5797    let entry = para.entry_at_position(rowan::TextSize::from(15));
5798    assert!(entry.is_some());
5799    assert_eq!(entry.unwrap().key(), Some("Version".to_string()));
5800
5801    // Position beyond paragraph
5802    let entry = para.entry_at_position(rowan::TextSize::from(1000));
5803    assert!(entry.is_none());
5804}
5805
5806#[test]
5807fn test_entry_at_position_multiline() {
5808    // Test finding entry in a multiline value
5809    let text = r#"Description: A package
5810 with a long
5811 description
5812"#;
5813    let deb822 = text.parse::<Deb822>().unwrap();
5814    let para = deb822.paragraphs().next().unwrap();
5815
5816    // Position 5 is within the Description entry
5817    let entry = para.entry_at_position(rowan::TextSize::from(5));
5818    assert!(entry.is_some());
5819    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5820
5821    // Position in continuation line should also find the Description entry
5822    let entry = para.entry_at_position(rowan::TextSize::from(30));
5823    assert!(entry.is_some());
5824    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5825}
5826
5827#[test]
5828fn test_paragraph_at_position_at_boundary() {
5829    // Test paragraph_at_position at paragraph boundaries
5830    let text = "Package: foo\n\nPackage: bar\n";
5831    let deb822 = text.parse::<Deb822>().unwrap();
5832
5833    // Position 0 is start of first paragraph
5834    let para = deb822.paragraph_at_position(rowan::TextSize::from(0));
5835    assert!(para.is_some());
5836    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5837
5838    // Position at start of second paragraph
5839    let para = deb822.paragraph_at_position(rowan::TextSize::from(15));
5840    assert!(para.is_some());
5841    assert_eq!(para.unwrap().get("Package").as_deref(), Some("bar"));
5842}
5843
5844#[test]
5845fn test_comment_in_multiline_value() {
5846    // Commented-out continuation lines within a multi-line field value
5847    // should be preserved losslessly and not cause parse errors.
5848    let text = "\
5849Build-Depends: dh-python,
5850               libsvn-dev,
5851#               python-all-dbg (>= 2.6.6-3),
5852               python3-all-dev,
5853#               python3-all-dbg,
5854               python3-docutils
5855Standards-Version: 4.7.0
5856";
5857    let deb822 = text.parse::<Deb822>().unwrap();
5858    let para = deb822.paragraphs().next().unwrap();
5859    // get() returns the value without comments
5860    assert_eq!(
5861        para.get("Build-Depends").as_deref(),
5862        Some("dh-python,\nlibsvn-dev,\npython3-all-dev,\npython3-docutils")
5863    );
5864    // get_with_comments() / value_with_comments() includes the comment lines
5865    assert_eq!(
5866        para.get_with_comments("Build-Depends").as_deref(),
5867        Some("dh-python,\nlibsvn-dev,\n#               python-all-dbg (>= 2.6.6-3),\npython3-all-dev,\n#               python3-all-dbg,\npython3-docutils")
5868    );
5869    assert_eq!(para.get("Standards-Version").as_deref(), Some("4.7.0"));
5870    // Round-trip
5871    assert_eq!(deb822.to_string(), text);
5872}