deb822_lossless/
lossless.rs

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