Skip to main content

debian_control/lossless/
control.rs

1//! This module provides a lossless representation of a Debian control file.
2//!
3//! # Example
4//! ```rust
5//! use debian_control::lossless::Control;
6//! use debian_control::relations::VersionConstraint;
7//! let input = r###"Source: dulwich
8//! ## Comments are preserved
9//! Maintainer: Jelmer Vernooij <jelmer@jelmer.uk>
10//! Build-Depends: python3, debhelper-compat (= 12)
11//!
12//! Package: python3-dulwich
13//! Architecture: amd64
14//! Description: Pure-python git implementation
15//! "###;
16//!
17//! let mut control: Control = input.parse().unwrap();
18//!
19//! // Bump debhelper-compat
20//! let source = control.source().unwrap();
21//! let bd = source.build_depends().unwrap();
22//!
23//! // Get entry with index 1 in Build-Depends, then set the version
24//! let entry = bd.get_entry(1).unwrap();
25//! let mut debhelper = entry.relations().next().unwrap();
26//! assert_eq!(debhelper.name(), "debhelper-compat");
27//! debhelper.set_version(Some((VersionConstraint::Equal, "13".parse().unwrap())));
28//!
29//! assert_eq!(source.to_string(), r###"Source: dulwich
30//! ## Comments are preserved
31//! Maintainer: Jelmer Vernooij <jelmer@jelmer.uk>
32//! Build-Depends: python3, debhelper-compat (= 12)
33//! "###);
34//! ```
35use crate::fields::{MultiArch, Priority};
36use crate::lossless::relations::Relations;
37use deb822_lossless::{Deb822, Paragraph, TextRange};
38use rowan::ast::AstNode;
39
40/// Parsing mode for Relations fields
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ParseMode {
43    /// Strict parsing - fail on syntax errors
44    Strict,
45    /// Relaxed parsing - accept syntax errors
46    Relaxed,
47    /// Allow substvars like ${misc:Depends}
48    Substvar,
49}
50
51/// Canonical field order for source paragraphs in debian/control files
52pub const SOURCE_FIELD_ORDER: &[&str] = &[
53    "Source",
54    "Section",
55    "Priority",
56    "Maintainer",
57    "Uploaders",
58    "Build-Depends",
59    "Build-Depends-Indep",
60    "Build-Depends-Arch",
61    "Build-Conflicts",
62    "Build-Conflicts-Indep",
63    "Build-Conflicts-Arch",
64    "Standards-Version",
65    "Vcs-Browser",
66    "Vcs-Git",
67    "Vcs-Svn",
68    "Vcs-Bzr",
69    "Vcs-Hg",
70    "Vcs-Darcs",
71    "Vcs-Cvs",
72    "Vcs-Arch",
73    "Vcs-Mtn",
74    "Homepage",
75    "Rules-Requires-Root",
76    "Testsuite",
77    "Testsuite-Triggers",
78];
79
80/// Canonical field order for binary packages in debian/control files
81pub const BINARY_FIELD_ORDER: &[&str] = &[
82    "Package",
83    "Architecture",
84    "Section",
85    "Priority",
86    "Multi-Arch",
87    "Essential",
88    "Build-Profiles",
89    "Built-Using",
90    "Static-Built-Using",
91    "Pre-Depends",
92    "Depends",
93    "Recommends",
94    "Suggests",
95    "Enhances",
96    "Conflicts",
97    "Breaks",
98    "Replaces",
99    "Provides",
100    "Description",
101];
102
103fn format_field(name: &str, value: &str, max_line_length_one_liner: Option<usize>) -> String {
104    match name {
105        "Uploaders" => value
106            .split(',')
107            .map(|s| s.trim().to_string())
108            .collect::<Vec<_>>()
109            .join(",\n"),
110        "Build-Depends"
111        | "Build-Depends-Indep"
112        | "Build-Depends-Arch"
113        | "Build-Conflicts"
114        | "Build-Conflicts-Indep"
115        | "Build-Conflics-Arch"
116        | "Depends"
117        | "Recommends"
118        | "Suggests"
119        | "Enhances"
120        | "Pre-Depends"
121        | "Breaks" => {
122            // Try to parse and format the relations.  If parsing fails,
123            // preserve the original value to maintain lossless behaviour.
124            let relations = match value.parse::<Relations>() {
125                Ok(r) => r.wrap_and_sort(),
126                Err(_) => return value.to_string(),
127            };
128            let one_line = relations.to_string();
129
130            // If the one-line form would exceed the requested max line
131            // length (including the field name and ": " prefix), join the
132            // parsed entries with ",\n" so rebuild_value will wrap them
133            // one-per-line.
134            if let Some(mll) = max_line_length_one_liner {
135                if name.len() + 2 + one_line.len() > mll {
136                    return relations
137                        .entries()
138                        .map(|e| e.to_string())
139                        .collect::<Vec<_>>()
140                        .join(",\n");
141                }
142            }
143            one_line
144        }
145        _ => value.to_string(),
146    }
147}
148
149/// A Debian control file
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct Control {
152    deb822: Deb822,
153    parse_mode: ParseMode,
154}
155
156impl Control {
157    /// Capture an independent snapshot of this Control file.
158    ///
159    /// The returned value shares the underlying immutable green-node data
160    /// with `self` at the time of the call, but lives in its own mutable
161    /// tree. Subsequent mutations to `self` do not propagate to the snapshot.
162    /// Pair with [`Self::tree_eq`] to detect later mutations.
163    pub fn snapshot(&self) -> Self {
164        Control {
165            deb822: self.deb822.snapshot(),
166            parse_mode: self.parse_mode,
167        }
168    }
169
170    /// Returns true iff the syntax trees of `self` and `other` are
171    /// value-equal. An O(1) pointer-identity fast path makes this free for
172    /// trees that still share state with a recent [`Self::snapshot`].
173    pub fn tree_eq(&self, other: &Self) -> bool {
174        self.deb822.tree_eq(&other.deb822)
175    }
176
177    /// Create a new control file with strict parsing
178    pub fn new() -> Self {
179        Control {
180            deb822: Deb822::new(),
181            parse_mode: ParseMode::Strict,
182        }
183    }
184
185    /// Create a new control file with the specified parse mode
186    pub fn new_with_mode(parse_mode: ParseMode) -> Self {
187        Control {
188            deb822: Deb822::new(),
189            parse_mode,
190        }
191    }
192
193    /// Get the parse mode for this control file
194    pub fn parse_mode(&self) -> ParseMode {
195        self.parse_mode
196    }
197
198    /// Return the underlying deb822 object, mutable
199    pub fn as_mut_deb822(&mut self) -> &mut Deb822 {
200        &mut self.deb822
201    }
202
203    /// Return the underlying deb822 object
204    pub fn as_deb822(&self) -> &Deb822 {
205        &self.deb822
206    }
207
208    /// Parse control file text, returning a Parse result
209    pub fn parse(text: &str) -> deb822_lossless::Parse<Control> {
210        let deb822_parse = Deb822::parse(text);
211        // Transform Parse<Deb822> to Parse<Control>
212        let green = deb822_parse.green().clone();
213        let errors = deb822_parse.errors().to_vec();
214        let positioned_errors = deb822_parse.positioned_errors().to_vec();
215        deb822_lossless::Parse::new_with_positioned_errors(green, errors, positioned_errors)
216    }
217
218    /// Return the source package
219    pub fn source(&self) -> Option<Source> {
220        let parse_mode = self.parse_mode;
221        self.deb822
222            .paragraphs()
223            .find(|p| p.get("Source").is_some())
224            .map(|paragraph| Source {
225                paragraph,
226                parse_mode,
227            })
228    }
229
230    /// Iterate over all binary packages
231    pub fn binaries(&self) -> impl Iterator<Item = Binary> + '_ {
232        let parse_mode = self.parse_mode;
233        self.deb822
234            .paragraphs()
235            .filter(|p| p.get("Package").is_some())
236            .map(move |paragraph| Binary {
237                paragraph,
238                parse_mode,
239            })
240    }
241
242    /// Return the source package if it intersects with the given text range
243    ///
244    /// # Arguments
245    /// * `range` - The text range to query
246    ///
247    /// # Returns
248    /// The source package if it exists and its text range overlaps with the provided range
249    pub fn source_in_range(&self, range: TextRange) -> Option<Source> {
250        self.source().filter(|s| {
251            let para_range = s.as_deb822().text_range();
252            para_range.start() < range.end() && para_range.end() > range.start()
253        })
254    }
255
256    /// Iterate over binary packages that intersect with the given text range
257    ///
258    /// # Arguments
259    /// * `range` - The text range to query
260    ///
261    /// # Returns
262    /// An iterator over binary packages whose text ranges overlap with the provided range
263    pub fn binaries_in_range(&self, range: TextRange) -> impl Iterator<Item = Binary> + '_ {
264        self.binaries().filter(move |b| {
265            let para_range = b.as_deb822().text_range();
266            para_range.start() < range.end() && para_range.end() > range.start()
267        })
268    }
269
270    /// Add a new source package
271    ///
272    /// # Arguments
273    /// * `name` - The name of the source package
274    ///
275    /// # Returns
276    /// The newly created source package
277    ///
278    /// # Example
279    /// ```rust
280    /// use debian_control::lossless::control::Control;
281    /// let mut control = Control::new();
282    /// let source = control.add_source("foo");
283    /// assert_eq!(source.name(), Some("foo".to_owned()));
284    /// ```
285    pub fn add_source(&mut self, name: &str) -> Source {
286        let mut p = self.deb822.add_paragraph();
287        p.set("Source", name);
288        self.source().unwrap()
289    }
290
291    /// Add new binary package
292    ///
293    /// # Arguments
294    /// * `name` - The name of the binary package
295    ///
296    /// # Returns
297    /// The newly created binary package
298    ///
299    /// # Example
300    /// ```rust
301    /// use debian_control::lossless::control::Control;
302    /// let mut control = Control::new();
303    /// let binary = control.add_binary("foo");
304    /// assert_eq!(binary.name(), Some("foo".to_owned()));
305    /// ```
306    pub fn add_binary(&mut self, name: &str) -> Binary {
307        let mut p = self.deb822.add_paragraph();
308        p.set("Package", name);
309        Binary {
310            paragraph: p,
311            parse_mode: ParseMode::Strict,
312        }
313    }
314
315    /// Remove a binary package paragraph by name
316    ///
317    /// # Arguments
318    /// * `name` - The name of the binary package to remove
319    ///
320    /// # Returns
321    /// `true` if a binary paragraph with the given name was found and removed, `false` otherwise
322    ///
323    /// # Example
324    /// ```rust
325    /// use debian_control::lossless::control::Control;
326    /// let mut control = Control::new();
327    /// control.add_binary("foo");
328    /// assert_eq!(control.binaries().count(), 1);
329    /// assert!(control.remove_binary("foo"));
330    /// assert_eq!(control.binaries().count(), 0);
331    /// ```
332    pub fn remove_binary(&mut self, name: &str) -> bool {
333        let index = self
334            .deb822
335            .paragraphs()
336            .position(|p| p.get("Package").as_deref() == Some(name));
337
338        if let Some(index) = index {
339            self.deb822.remove_paragraph(index);
340            true
341        } else {
342            false
343        }
344    }
345
346    /// Read a control file from a file
347    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, deb822_lossless::Error> {
348        Ok(Control {
349            deb822: Deb822::from_file(path)?,
350            parse_mode: ParseMode::Strict,
351        })
352    }
353
354    /// Read a control file from a file, allowing syntax errors
355    pub fn from_file_relaxed<P: AsRef<std::path::Path>>(
356        path: P,
357    ) -> Result<(Self, Vec<String>), std::io::Error> {
358        let (deb822, errors) = Deb822::from_file_relaxed(path)?;
359        Ok((
360            Control {
361                deb822,
362                parse_mode: ParseMode::Relaxed,
363            },
364            errors,
365        ))
366    }
367
368    /// Read a control file from a reader
369    pub fn read<R: std::io::Read>(mut r: R) -> Result<Self, deb822_lossless::Error> {
370        Ok(Control {
371            deb822: Deb822::read(&mut r)?,
372            parse_mode: ParseMode::Strict,
373        })
374    }
375
376    /// Read a control file from a reader, allowing syntax errors
377    pub fn read_relaxed<R: std::io::Read>(
378        mut r: R,
379    ) -> Result<(Self, Vec<String>), deb822_lossless::Error> {
380        let (deb822, errors) = Deb822::read_relaxed(&mut r)?;
381        Ok((
382            Control {
383                deb822,
384                parse_mode: ParseMode::Relaxed,
385            },
386            errors,
387        ))
388    }
389
390    /// Wrap and sort the control file
391    ///
392    /// # Arguments
393    /// * `indentation` - The indentation to use
394    /// * `immediate_empty_line` - Whether to add an empty line at the start of multi-line fields
395    /// * `max_line_length_one_liner` - The maximum line length for one-liner fields
396    pub fn wrap_and_sort(
397        &mut self,
398        indentation: deb822_lossless::Indentation,
399        immediate_empty_line: bool,
400        max_line_length_one_liner: Option<usize>,
401    ) {
402        let sort_paragraphs = |a: &Paragraph, b: &Paragraph| -> std::cmp::Ordering {
403            // Sort Source before Package
404            let a_is_source = a.get("Source").is_some();
405            let b_is_source = b.get("Source").is_some();
406
407            if a_is_source && !b_is_source {
408                return std::cmp::Ordering::Less;
409            } else if !a_is_source && b_is_source {
410                return std::cmp::Ordering::Greater;
411            } else if a_is_source && b_is_source {
412                return a.get("Source").cmp(&b.get("Source"));
413            }
414
415            a.get("Package").cmp(&b.get("Package"))
416        };
417
418        let format = |name: &str, value: &str| -> String {
419            format_field(name, value, max_line_length_one_liner)
420        };
421        let wrap_paragraph = |p: &Paragraph| -> Paragraph {
422            // TODO: Add Source/Package specific wrapping
423            // TODO: Add support for wrapping and sorting fields
424            p.wrap_and_sort(
425                indentation,
426                immediate_empty_line,
427                max_line_length_one_liner,
428                None,
429                Some(&format),
430            )
431        };
432
433        self.deb822 = self
434            .deb822
435            .wrap_and_sort(Some(&sort_paragraphs), Some(&wrap_paragraph));
436    }
437
438    /// Sort binary package paragraphs alphabetically by package name.
439    ///
440    /// This method reorders the binary package paragraphs in alphabetical order
441    /// based on their Package field value. The source paragraph always remains first.
442    ///
443    /// # Arguments
444    /// * `keep_first` - If true, keeps the first binary package in place and only
445    ///   sorts the remaining binary packages. If false, sorts all binary packages.
446    ///
447    /// # Example
448    /// ```rust
449    /// use debian_control::lossless::Control;
450    ///
451    /// let input = r#"Source: foo
452    ///
453    /// Package: libfoo
454    /// Architecture: all
455    ///
456    /// Package: libbar
457    /// Architecture: all
458    /// "#;
459    ///
460    /// let mut control: Control = input.parse().unwrap();
461    /// control.sort_binaries(false);
462    ///
463    /// // Binary packages are now sorted: libbar comes before libfoo
464    /// let binaries: Vec<_> = control.binaries().collect();
465    /// assert_eq!(binaries[0].name(), Some("libbar".to_string()));
466    /// assert_eq!(binaries[1].name(), Some("libfoo".to_string()));
467    /// ```
468    pub fn sort_binaries(&mut self, keep_first: bool) {
469        let mut paragraphs: Vec<_> = self.deb822.paragraphs().collect();
470
471        if paragraphs.len() <= 1 {
472            return; // Only source paragraph, nothing to sort
473        }
474
475        // Find the index where binary packages start (after source)
476        let source_idx = paragraphs.iter().position(|p| p.get("Source").is_some());
477        let binary_start = source_idx.map(|i| i + 1).unwrap_or(0);
478
479        // Determine where to start sorting
480        let sort_start = if keep_first && paragraphs.len() > binary_start + 1 {
481            binary_start + 1
482        } else {
483            binary_start
484        };
485
486        if sort_start >= paragraphs.len() {
487            return; // Nothing to sort
488        }
489
490        // Sort binary packages by package name
491        paragraphs[sort_start..].sort_by(|a, b| {
492            let a_name = a.get("Package");
493            let b_name = b.get("Package");
494            a_name.cmp(&b_name)
495        });
496
497        // Rebuild the Deb822 with sorted paragraphs
498        let sort_paragraphs = |a: &Paragraph, b: &Paragraph| -> std::cmp::Ordering {
499            let a_pos = paragraphs.iter().position(|p| p == a);
500            let b_pos = paragraphs.iter().position(|p| p == b);
501            a_pos.cmp(&b_pos)
502        };
503
504        self.deb822 = self.deb822.wrap_and_sort(Some(&sort_paragraphs), None);
505    }
506
507    /// Iterate over fields that overlap with the given range
508    ///
509    /// This method returns all fields (entries) from all paragraphs that have any overlap
510    /// with the specified text range. This is useful for incremental parsing in LSP contexts
511    /// where you only want to process fields that were affected by a text change.
512    ///
513    /// # Arguments
514    /// * `range` - The text range to check for overlaps
515    ///
516    /// # Returns
517    /// An iterator over all Entry items that overlap with the given range
518    ///
519    /// # Example
520    /// ```rust
521    /// use debian_control::lossless::Control;
522    /// use deb822_lossless::TextRange;
523    ///
524    /// let control_text = "Source: foo\nMaintainer: test@example.com\n\nPackage: bar\nArchitecture: all\n";
525    /// let control: Control = control_text.parse().unwrap();
526    ///
527    /// // Get fields in a specific range (e.g., where a change occurred)
528    /// let change_range = TextRange::new(20.into(), 40.into());
529    /// for entry in control.fields_in_range(change_range) {
530    ///     if let Some(key) = entry.key() {
531    ///         println!("Field {} was in the changed range", key);
532    ///     }
533    /// }
534    /// ```
535    pub fn fields_in_range(
536        &self,
537        range: TextRange,
538    ) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
539        self.deb822
540            .paragraphs()
541            .flat_map(move |p| p.entries().collect::<Vec<_>>())
542            .filter(move |entry| {
543                let entry_range = entry.syntax().text_range();
544                // Check if ranges overlap
545                entry_range.start() < range.end() && range.start() < entry_range.end()
546            })
547    }
548}
549
550impl From<Control> for Deb822 {
551    fn from(c: Control) -> Self {
552        c.deb822
553    }
554}
555
556impl From<Deb822> for Control {
557    fn from(d: Deb822) -> Self {
558        Control {
559            deb822: d,
560            parse_mode: ParseMode::Strict,
561        }
562    }
563}
564
565impl Default for Control {
566    fn default() -> Self {
567        Self::new()
568    }
569}
570
571impl std::str::FromStr for Control {
572    type Err = deb822_lossless::ParseError;
573
574    fn from_str(s: &str) -> Result<Self, Self::Err> {
575        Control::parse(s).to_result()
576    }
577}
578
579/// A source package paragraph
580#[derive(Debug, Clone, PartialEq, Eq)]
581pub struct Source {
582    paragraph: Paragraph,
583    parse_mode: ParseMode,
584}
585
586impl From<Source> for Paragraph {
587    fn from(s: Source) -> Self {
588        s.paragraph
589    }
590}
591
592impl From<Paragraph> for Source {
593    fn from(p: Paragraph) -> Self {
594        Source {
595            paragraph: p,
596            parse_mode: ParseMode::Strict,
597        }
598    }
599}
600
601impl std::fmt::Display for Source {
602    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
603        self.paragraph.fmt(f)
604    }
605}
606
607impl Source {
608    /// Parse a relations field according to the parse mode
609    fn parse_relations(&self, s: &str) -> Relations {
610        match self.parse_mode {
611            ParseMode::Strict => s.parse().unwrap(),
612            ParseMode::Relaxed => Relations::parse_relaxed(s, false).0,
613            ParseMode::Substvar => Relations::parse_relaxed(s, true).0,
614        }
615    }
616
617    /// The name of the source package.
618    pub fn name(&self) -> Option<String> {
619        self.paragraph.get("Source")
620    }
621
622    /// Wrap and sort the control file paragraph
623    pub fn wrap_and_sort(
624        &mut self,
625        indentation: deb822_lossless::Indentation,
626        immediate_empty_line: bool,
627        max_line_length_one_liner: Option<usize>,
628    ) {
629        let format = |name: &str, value: &str| -> String {
630            format_field(name, value, max_line_length_one_liner)
631        };
632        self.paragraph = self.paragraph.wrap_and_sort(
633            indentation,
634            immediate_empty_line,
635            max_line_length_one_liner,
636            None,
637            Some(&format),
638        );
639    }
640
641    /// Return the underlying deb822 paragraph, mutable
642    pub fn as_mut_deb822(&mut self) -> &mut Paragraph {
643        &mut self.paragraph
644    }
645
646    /// Return the underlying deb822 paragraph
647    pub fn as_deb822(&self) -> &Paragraph {
648        &self.paragraph
649    }
650
651    /// Set the name of the source package.
652    pub fn set_name(&mut self, name: &str) {
653        self.set("Source", name);
654    }
655
656    /// The default section of the packages built from this source package.
657    pub fn section(&self) -> Option<String> {
658        self.paragraph.get("Section")
659    }
660
661    /// Set the section of the source package
662    pub fn set_section(&mut self, section: Option<&str>) {
663        if let Some(section) = section {
664            self.set("Section", section);
665        } else {
666            self.paragraph.remove("Section");
667        }
668    }
669
670    /// The default priority of the packages built from this source package.
671    pub fn priority(&self) -> Option<Priority> {
672        self.paragraph.get("Priority").and_then(|v| v.parse().ok())
673    }
674
675    /// Set the priority of the source package
676    pub fn set_priority(&mut self, priority: Option<Priority>) {
677        if let Some(priority) = priority {
678            self.set("Priority", priority.to_string().as_str());
679        } else {
680            self.paragraph.remove("Priority");
681        }
682    }
683
684    /// The maintainer of the package.
685    pub fn maintainer(&self) -> Option<String> {
686        self.paragraph.get("Maintainer")
687    }
688
689    /// Set the maintainer of the package
690    pub fn set_maintainer(&mut self, maintainer: &str) {
691        self.set("Maintainer", maintainer);
692    }
693
694    /// The build dependencies of the package.
695    pub fn build_depends(&self) -> Option<Relations> {
696        self.paragraph
697            .get_with_comments("Build-Depends")
698            .map(|s| self.parse_relations(&s))
699    }
700
701    /// Set the Build-Depends field
702    pub fn set_build_depends(&mut self, relations: &Relations) {
703        self.set("Build-Depends", relations.to_string().as_str());
704    }
705
706    /// Return the Build-Depends-Indep field
707    pub fn build_depends_indep(&self) -> Option<Relations> {
708        self.paragraph
709            .get_with_comments("Build-Depends-Indep")
710            .map(|s| self.parse_relations(&s))
711    }
712
713    /// Set the Build-Depends-Indep field
714    pub fn set_build_depends_indep(&mut self, relations: &Relations) {
715        self.set("Build-Depends-Indep", relations.to_string().as_str());
716    }
717
718    /// Return the Build-Depends-Arch field
719    pub fn build_depends_arch(&self) -> Option<Relations> {
720        self.paragraph
721            .get_with_comments("Build-Depends-Arch")
722            .map(|s| self.parse_relations(&s))
723    }
724
725    /// Set the Build-Depends-Arch field
726    pub fn set_build_depends_arch(&mut self, relations: &Relations) {
727        self.set("Build-Depends-Arch", relations.to_string().as_str());
728    }
729
730    /// The build conflicts of the package.
731    pub fn build_conflicts(&self) -> Option<Relations> {
732        self.paragraph
733            .get_with_comments("Build-Conflicts")
734            .map(|s| self.parse_relations(&s))
735    }
736
737    /// Set the Build-Conflicts field
738    pub fn set_build_conflicts(&mut self, relations: &Relations) {
739        self.set("Build-Conflicts", relations.to_string().as_str());
740    }
741
742    /// Return the Build-Conflicts-Indep field
743    pub fn build_conflicts_indep(&self) -> Option<Relations> {
744        self.paragraph
745            .get_with_comments("Build-Conflicts-Indep")
746            .map(|s| self.parse_relations(&s))
747    }
748
749    /// Set the Build-Conflicts-Indep field
750    pub fn set_build_conflicts_indep(&mut self, relations: &Relations) {
751        self.set("Build-Conflicts-Indep", relations.to_string().as_str());
752    }
753
754    /// Return the Build-Conflicts-Arch field
755    pub fn build_conflicts_arch(&self) -> Option<Relations> {
756        self.paragraph
757            .get_with_comments("Build-Conflicts-Arch")
758            .map(|s| self.parse_relations(&s))
759    }
760
761    /// Return the standards version
762    pub fn standards_version(&self) -> Option<String> {
763        self.paragraph.get("Standards-Version")
764    }
765
766    /// Set the Standards-Version field
767    pub fn set_standards_version(&mut self, version: &str) {
768        self.set("Standards-Version", version);
769    }
770
771    /// Return the upstrea mHomepage
772    pub fn homepage(&self) -> Option<url::Url> {
773        self.paragraph.get("Homepage").and_then(|s| s.parse().ok())
774    }
775
776    /// Set the Homepage field
777    pub fn set_homepage(&mut self, homepage: &url::Url) {
778        self.set("Homepage", homepage.to_string().as_str());
779    }
780
781    /// Return the Vcs-Git field
782    pub fn vcs_git(&self) -> Option<String> {
783        self.paragraph.get("Vcs-Git")
784    }
785
786    /// Set the Vcs-Git field
787    pub fn set_vcs_git(&mut self, url: &str) {
788        self.set("Vcs-Git", url);
789    }
790
791    /// Return the Vcs-Browser field
792    pub fn vcs_svn(&self) -> Option<String> {
793        self.paragraph.get("Vcs-Svn").map(|s| s.to_string())
794    }
795
796    /// Set the Vcs-Svn field
797    pub fn set_vcs_svn(&mut self, url: &str) {
798        self.set("Vcs-Svn", url);
799    }
800
801    /// Return the Vcs-Bzr field
802    pub fn vcs_bzr(&self) -> Option<String> {
803        self.paragraph.get("Vcs-Bzr").map(|s| s.to_string())
804    }
805
806    /// Set the Vcs-Bzr field
807    pub fn set_vcs_bzr(&mut self, url: &str) {
808        self.set("Vcs-Bzr", url);
809    }
810
811    /// Return the Vcs-Arch field
812    pub fn vcs_arch(&self) -> Option<String> {
813        self.paragraph.get("Vcs-Arch").map(|s| s.to_string())
814    }
815
816    /// Set the Vcs-Arch field
817    pub fn set_vcs_arch(&mut self, url: &str) {
818        self.set("Vcs-Arch", url);
819    }
820
821    /// Return the Vcs-Svk field
822    pub fn vcs_svk(&self) -> Option<String> {
823        self.paragraph.get("Vcs-Svk").map(|s| s.to_string())
824    }
825
826    /// Set the Vcs-Svk field
827    pub fn set_vcs_svk(&mut self, url: &str) {
828        self.set("Vcs-Svk", url);
829    }
830
831    /// Return the Vcs-Darcs field
832    pub fn vcs_darcs(&self) -> Option<String> {
833        self.paragraph.get("Vcs-Darcs").map(|s| s.to_string())
834    }
835
836    /// Set the Vcs-Darcs field
837    pub fn set_vcs_darcs(&mut self, url: &str) {
838        self.set("Vcs-Darcs", url);
839    }
840
841    /// Return the Vcs-Mtn field
842    pub fn vcs_mtn(&self) -> Option<String> {
843        self.paragraph.get("Vcs-Mtn").map(|s| s.to_string())
844    }
845
846    /// Set the Vcs-Mtn field
847    pub fn set_vcs_mtn(&mut self, url: &str) {
848        self.set("Vcs-Mtn", url);
849    }
850
851    /// Return the Vcs-Cvs field
852    pub fn vcs_cvs(&self) -> Option<String> {
853        self.paragraph.get("Vcs-Cvs").map(|s| s.to_string())
854    }
855
856    /// Set the Vcs-Cvs field
857    pub fn set_vcs_cvs(&mut self, url: &str) {
858        self.set("Vcs-Cvs", url);
859    }
860
861    /// Return the Vcs-Hg field
862    pub fn vcs_hg(&self) -> Option<String> {
863        self.paragraph.get("Vcs-Hg").map(|s| s.to_string())
864    }
865
866    /// Set the Vcs-Hg field
867    pub fn set_vcs_hg(&mut self, url: &str) {
868        self.set("Vcs-Hg", url);
869    }
870
871    /// Set a field in the source paragraph, using canonical field ordering for source packages
872    pub fn set(&mut self, key: &str, value: &str) {
873        self.paragraph
874            .set_with_field_order(key, value, SOURCE_FIELD_ORDER);
875    }
876
877    /// Retrieve a field
878    pub fn get(&self, key: &str) -> Option<String> {
879        self.paragraph.get(key)
880    }
881
882    /// Return the Vcs-Browser field
883    pub fn vcs_browser(&self) -> Option<String> {
884        self.paragraph.get("Vcs-Browser")
885    }
886
887    /// Return the Vcs used by the package
888    pub fn vcs(&self) -> Option<crate::vcs::Vcs> {
889        for (name, value) in self.paragraph.items() {
890            if name.starts_with("Vcs-") && name != "Vcs-Browser" {
891                return crate::vcs::Vcs::from_field(&name, &value).ok();
892            }
893        }
894        None
895    }
896
897    /// Set the Vcs-Browser field
898    pub fn set_vcs_browser(&mut self, url: Option<&str>) {
899        if let Some(url) = url {
900            self.set("Vcs-Browser", url);
901        } else {
902            self.paragraph.remove("Vcs-Browser");
903        }
904    }
905
906    /// Return the Uploaders field
907    pub fn uploaders(&self) -> Option<Vec<String>> {
908        self.paragraph
909            .get("Uploaders")
910            .map(|s| s.split(',').map(|s| s.trim().to_owned()).collect())
911    }
912
913    /// Set the uploaders field
914    pub fn set_uploaders(&mut self, uploaders: &[&str]) {
915        self.set(
916            "Uploaders",
917            uploaders
918                .iter()
919                .map(|s| s.to_string())
920                .collect::<Vec<_>>()
921                .join(", ")
922                .as_str(),
923        );
924    }
925
926    /// Return the architecture field
927    pub fn architecture(&self) -> Option<String> {
928        self.paragraph.get("Architecture")
929    }
930
931    /// Set the architecture field
932    pub fn set_architecture(&mut self, arch: Option<&str>) {
933        if let Some(arch) = arch {
934            self.set("Architecture", arch);
935        } else {
936            self.paragraph.remove("Architecture");
937        }
938    }
939
940    /// Return the Rules-Requires-Root field
941    pub fn rules_requires_root(&self) -> Option<bool> {
942        self.paragraph
943            .get("Rules-Requires-Root")
944            .map(|s| match s.to_lowercase().as_str() {
945                "yes" => true,
946                "no" => false,
947                _ => panic!("invalid Rules-Requires-Root value"),
948            })
949    }
950
951    /// Set the Rules-Requires-Root field
952    pub fn set_rules_requires_root(&mut self, requires_root: bool) {
953        self.set(
954            "Rules-Requires-Root",
955            if requires_root { "yes" } else { "no" },
956        );
957    }
958
959    /// Return the Testsuite field
960    pub fn testsuite(&self) -> Option<String> {
961        self.paragraph.get("Testsuite")
962    }
963
964    /// Set the Testsuite field
965    pub fn set_testsuite(&mut self, testsuite: &str) {
966        self.set("Testsuite", testsuite);
967    }
968
969    /// Check if this source paragraph's range overlaps with the given range
970    ///
971    /// # Arguments
972    /// * `range` - The text range to check for overlap
973    ///
974    /// # Returns
975    /// `true` if the paragraph overlaps with the given range, `false` otherwise
976    pub fn overlaps_range(&self, range: TextRange) -> bool {
977        let para_range = self.paragraph.syntax().text_range();
978        para_range.start() < range.end() && range.start() < para_range.end()
979    }
980
981    /// Get fields in this source paragraph that overlap with the given range
982    ///
983    /// # Arguments
984    /// * `range` - The text range to check for overlaps
985    ///
986    /// # Returns
987    /// An iterator over Entry items that overlap with the given range
988    pub fn fields_in_range(
989        &self,
990        range: TextRange,
991    ) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
992        self.paragraph.entries().filter(move |entry| {
993            let entry_range = entry.syntax().text_range();
994            entry_range.start() < range.end() && range.start() < entry_range.end()
995        })
996    }
997}
998
999#[cfg(feature = "python-debian")]
1000impl<'py> pyo3::IntoPyObject<'py> for Source {
1001    type Target = pyo3::PyAny;
1002    type Output = pyo3::Bound<'py, Self::Target>;
1003    type Error = pyo3::PyErr;
1004
1005    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
1006        self.paragraph.into_pyobject(py)
1007    }
1008}
1009
1010#[cfg(feature = "python-debian")]
1011impl<'py> pyo3::IntoPyObject<'py> for &Source {
1012    type Target = pyo3::PyAny;
1013    type Output = pyo3::Bound<'py, Self::Target>;
1014    type Error = pyo3::PyErr;
1015
1016    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
1017        (&self.paragraph).into_pyobject(py)
1018    }
1019}
1020
1021#[cfg(feature = "python-debian")]
1022impl<'py> pyo3::FromPyObject<'_, 'py> for Source {
1023    type Error = pyo3::PyErr;
1024
1025    fn extract(ob: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
1026        Ok(Source {
1027            paragraph: ob.extract()?,
1028            parse_mode: ParseMode::Strict,
1029        })
1030    }
1031}
1032
1033impl std::fmt::Display for Control {
1034    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1035        self.deb822.fmt(f)
1036    }
1037}
1038
1039impl AstNode for Control {
1040    type Language = deb822_lossless::Lang;
1041
1042    fn can_cast(kind: <Self::Language as rowan::Language>::Kind) -> bool {
1043        Deb822::can_cast(kind)
1044    }
1045
1046    fn cast(syntax: rowan::SyntaxNode<Self::Language>) -> Option<Self> {
1047        Deb822::cast(syntax).map(|deb822| Control {
1048            deb822,
1049            parse_mode: ParseMode::Strict,
1050        })
1051    }
1052
1053    fn syntax(&self) -> &rowan::SyntaxNode<Self::Language> {
1054        self.deb822.syntax()
1055    }
1056}
1057
1058/// A binary package paragraph
1059#[derive(Debug, Clone, PartialEq, Eq)]
1060pub struct Binary {
1061    paragraph: Paragraph,
1062    parse_mode: ParseMode,
1063}
1064
1065impl From<Binary> for Paragraph {
1066    fn from(b: Binary) -> Self {
1067        b.paragraph
1068    }
1069}
1070
1071impl From<Paragraph> for Binary {
1072    fn from(p: Paragraph) -> Self {
1073        Binary {
1074            paragraph: p,
1075            parse_mode: ParseMode::Strict,
1076        }
1077    }
1078}
1079
1080#[cfg(feature = "python-debian")]
1081impl<'py> pyo3::IntoPyObject<'py> for Binary {
1082    type Target = pyo3::PyAny;
1083    type Output = pyo3::Bound<'py, Self::Target>;
1084    type Error = pyo3::PyErr;
1085
1086    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
1087        self.paragraph.into_pyobject(py)
1088    }
1089}
1090
1091#[cfg(feature = "python-debian")]
1092impl<'py> pyo3::IntoPyObject<'py> for &Binary {
1093    type Target = pyo3::PyAny;
1094    type Output = pyo3::Bound<'py, Self::Target>;
1095    type Error = pyo3::PyErr;
1096
1097    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
1098        (&self.paragraph).into_pyobject(py)
1099    }
1100}
1101
1102#[cfg(feature = "python-debian")]
1103impl<'py> pyo3::FromPyObject<'_, 'py> for Binary {
1104    type Error = pyo3::PyErr;
1105
1106    fn extract(ob: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
1107        Ok(Binary {
1108            paragraph: ob.extract()?,
1109            parse_mode: ParseMode::Strict,
1110        })
1111    }
1112}
1113
1114impl Default for Binary {
1115    fn default() -> Self {
1116        Self::new()
1117    }
1118}
1119
1120impl std::fmt::Display for Binary {
1121    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1122        self.paragraph.fmt(f)
1123    }
1124}
1125
1126impl Binary {
1127    /// Parse a relations field according to the parse mode
1128    fn parse_relations(&self, s: &str) -> Relations {
1129        match self.parse_mode {
1130            ParseMode::Strict => s.parse().unwrap(),
1131            ParseMode::Relaxed => Relations::parse_relaxed(s, false).0,
1132            ParseMode::Substvar => Relations::parse_relaxed(s, true).0,
1133        }
1134    }
1135
1136    /// Create a new binary package control file
1137    pub fn new() -> Self {
1138        Binary {
1139            paragraph: Paragraph::new(),
1140            parse_mode: ParseMode::Strict,
1141        }
1142    }
1143
1144    /// Return the underlying deb822 paragraph, mutable
1145    pub fn as_mut_deb822(&mut self) -> &mut Paragraph {
1146        &mut self.paragraph
1147    }
1148
1149    /// Return the underlying deb822 paragraph
1150    pub fn as_deb822(&self) -> &Paragraph {
1151        &self.paragraph
1152    }
1153
1154    /// Wrap and sort the control file
1155    pub fn wrap_and_sort(
1156        &mut self,
1157        indentation: deb822_lossless::Indentation,
1158        immediate_empty_line: bool,
1159        max_line_length_one_liner: Option<usize>,
1160    ) {
1161        let format = |name: &str, value: &str| -> String {
1162            format_field(name, value, max_line_length_one_liner)
1163        };
1164        self.paragraph = self.paragraph.wrap_and_sort(
1165            indentation,
1166            immediate_empty_line,
1167            max_line_length_one_liner,
1168            None,
1169            Some(&format),
1170        );
1171    }
1172
1173    /// The name of the package.
1174    pub fn name(&self) -> Option<String> {
1175        self.paragraph.get("Package")
1176    }
1177
1178    /// Set the name of the package
1179    pub fn set_name(&mut self, name: &str) {
1180        self.set("Package", name);
1181    }
1182
1183    /// The section of the package.
1184    pub fn section(&self) -> Option<String> {
1185        self.paragraph.get("Section")
1186    }
1187
1188    /// Set the section
1189    pub fn set_section(&mut self, section: Option<&str>) {
1190        if let Some(section) = section {
1191            self.set("Section", section);
1192        } else {
1193            self.paragraph.remove("Section");
1194        }
1195    }
1196
1197    /// The priority of the package.
1198    pub fn priority(&self) -> Option<Priority> {
1199        self.paragraph.get("Priority").and_then(|v| v.parse().ok())
1200    }
1201
1202    /// Set the priority of the package
1203    pub fn set_priority(&mut self, priority: Option<Priority>) {
1204        if let Some(priority) = priority {
1205            self.set("Priority", priority.to_string().as_str());
1206        } else {
1207            self.paragraph.remove("Priority");
1208        }
1209    }
1210
1211    /// The architecture of the package.
1212    pub fn architecture(&self) -> Option<String> {
1213        self.paragraph.get("Architecture")
1214    }
1215
1216    /// Set the architecture of the package
1217    pub fn set_architecture(&mut self, arch: Option<&str>) {
1218        if let Some(arch) = arch {
1219            self.set("Architecture", arch);
1220        } else {
1221            self.paragraph.remove("Architecture");
1222        }
1223    }
1224
1225    /// The dependencies of the package.
1226    pub fn depends(&self) -> Option<Relations> {
1227        self.paragraph
1228            .get_with_comments("Depends")
1229            .map(|s| self.parse_relations(&s))
1230    }
1231
1232    /// Set the Depends field
1233    pub fn set_depends(&mut self, depends: Option<&Relations>) {
1234        if let Some(depends) = depends {
1235            self.set("Depends", depends.to_string().as_str());
1236        } else {
1237            self.paragraph.remove("Depends");
1238        }
1239    }
1240
1241    /// The package that this package recommends
1242    pub fn recommends(&self) -> Option<Relations> {
1243        self.paragraph
1244            .get_with_comments("Recommends")
1245            .map(|s| self.parse_relations(&s))
1246    }
1247
1248    /// Set the Recommends field
1249    pub fn set_recommends(&mut self, recommends: Option<&Relations>) {
1250        if let Some(recommends) = recommends {
1251            self.set("Recommends", recommends.to_string().as_str());
1252        } else {
1253            self.paragraph.remove("Recommends");
1254        }
1255    }
1256
1257    /// Packages that this package suggests
1258    pub fn suggests(&self) -> Option<Relations> {
1259        self.paragraph
1260            .get_with_comments("Suggests")
1261            .map(|s| self.parse_relations(&s))
1262    }
1263
1264    /// Set the Suggests field
1265    pub fn set_suggests(&mut self, suggests: Option<&Relations>) {
1266        if let Some(suggests) = suggests {
1267            self.set("Suggests", suggests.to_string().as_str());
1268        } else {
1269            self.paragraph.remove("Suggests");
1270        }
1271    }
1272
1273    /// The package that this package enhances
1274    pub fn enhances(&self) -> Option<Relations> {
1275        self.paragraph
1276            .get_with_comments("Enhances")
1277            .map(|s| self.parse_relations(&s))
1278    }
1279
1280    /// Set the Enhances field
1281    pub fn set_enhances(&mut self, enhances: Option<&Relations>) {
1282        if let Some(enhances) = enhances {
1283            self.set("Enhances", enhances.to_string().as_str());
1284        } else {
1285            self.paragraph.remove("Enhances");
1286        }
1287    }
1288
1289    /// The package that this package pre-depends on
1290    pub fn pre_depends(&self) -> Option<Relations> {
1291        self.paragraph
1292            .get_with_comments("Pre-Depends")
1293            .map(|s| self.parse_relations(&s))
1294    }
1295
1296    /// Set the Pre-Depends field
1297    pub fn set_pre_depends(&mut self, pre_depends: Option<&Relations>) {
1298        if let Some(pre_depends) = pre_depends {
1299            self.set("Pre-Depends", pre_depends.to_string().as_str());
1300        } else {
1301            self.paragraph.remove("Pre-Depends");
1302        }
1303    }
1304
1305    /// The package that this package breaks
1306    pub fn breaks(&self) -> Option<Relations> {
1307        self.paragraph
1308            .get_with_comments("Breaks")
1309            .map(|s| self.parse_relations(&s))
1310    }
1311
1312    /// Set the Breaks field
1313    pub fn set_breaks(&mut self, breaks: Option<&Relations>) {
1314        if let Some(breaks) = breaks {
1315            self.set("Breaks", breaks.to_string().as_str());
1316        } else {
1317            self.paragraph.remove("Breaks");
1318        }
1319    }
1320
1321    /// The package that this package conflicts with
1322    pub fn conflicts(&self) -> Option<Relations> {
1323        self.paragraph
1324            .get_with_comments("Conflicts")
1325            .map(|s| self.parse_relations(&s))
1326    }
1327
1328    /// Set the Conflicts field
1329    pub fn set_conflicts(&mut self, conflicts: Option<&Relations>) {
1330        if let Some(conflicts) = conflicts {
1331            self.set("Conflicts", conflicts.to_string().as_str());
1332        } else {
1333            self.paragraph.remove("Conflicts");
1334        }
1335    }
1336
1337    /// The package that this package replaces
1338    pub fn replaces(&self) -> Option<Relations> {
1339        self.paragraph
1340            .get_with_comments("Replaces")
1341            .map(|s| self.parse_relations(&s))
1342    }
1343
1344    /// Set the Replaces field
1345    pub fn set_replaces(&mut self, replaces: Option<&Relations>) {
1346        if let Some(replaces) = replaces {
1347            self.set("Replaces", replaces.to_string().as_str());
1348        } else {
1349            self.paragraph.remove("Replaces");
1350        }
1351    }
1352
1353    /// Return the Provides field
1354    pub fn provides(&self) -> Option<Relations> {
1355        self.paragraph
1356            .get_with_comments("Provides")
1357            .map(|s| self.parse_relations(&s))
1358    }
1359
1360    /// Set the Provides field
1361    pub fn set_provides(&mut self, provides: Option<&Relations>) {
1362        if let Some(provides) = provides {
1363            self.set("Provides", provides.to_string().as_str());
1364        } else {
1365            self.paragraph.remove("Provides");
1366        }
1367    }
1368
1369    /// Return the Built-Using field
1370    pub fn built_using(&self) -> Option<Relations> {
1371        self.paragraph
1372            .get_with_comments("Built-Using")
1373            .map(|s| self.parse_relations(&s))
1374    }
1375
1376    /// Set the Built-Using field
1377    pub fn set_built_using(&mut self, built_using: Option<&Relations>) {
1378        if let Some(built_using) = built_using {
1379            self.set("Built-Using", built_using.to_string().as_str());
1380        } else {
1381            self.paragraph.remove("Built-Using");
1382        }
1383    }
1384
1385    /// Return the Static-Built-Using field
1386    pub fn static_built_using(&self) -> Option<Relations> {
1387        self.paragraph
1388            .get_with_comments("Static-Built-Using")
1389            .map(|s| self.parse_relations(&s))
1390    }
1391
1392    /// Set the Static-Built-Using field
1393    pub fn set_static_built_using(&mut self, static_built_using: Option<&Relations>) {
1394        if let Some(static_built_using) = static_built_using {
1395            self.set(
1396                "Static-Built-Using",
1397                static_built_using.to_string().as_str(),
1398            );
1399        } else {
1400            self.paragraph.remove("Static-Built-Using");
1401        }
1402    }
1403
1404    /// The Multi-Arch field
1405    pub fn multi_arch(&self) -> Option<MultiArch> {
1406        self.paragraph.get("Multi-Arch").map(|s| s.parse().unwrap())
1407    }
1408
1409    /// Set the Multi-Arch field
1410    pub fn set_multi_arch(&mut self, multi_arch: Option<MultiArch>) {
1411        if let Some(multi_arch) = multi_arch {
1412            self.set("Multi-Arch", multi_arch.to_string().as_str());
1413        } else {
1414            self.paragraph.remove("Multi-Arch");
1415        }
1416    }
1417
1418    /// Whether the package is essential
1419    pub fn essential(&self) -> bool {
1420        self.paragraph
1421            .get("Essential")
1422            .map(|s| s == "yes")
1423            .unwrap_or(false)
1424    }
1425
1426    /// Set whether the package is essential
1427    pub fn set_essential(&mut self, essential: bool) {
1428        if essential {
1429            self.set("Essential", "yes");
1430        } else {
1431            self.paragraph.remove("Essential");
1432        }
1433    }
1434
1435    /// Binary package description
1436    pub fn description(&self) -> Option<String> {
1437        self.paragraph.get_multiline("Description")
1438    }
1439
1440    /// Set the binary package description
1441    pub fn set_description(&mut self, description: Option<&str>) {
1442        if let Some(description) = description {
1443            self.paragraph.set_with_indent_pattern(
1444                "Description",
1445                description,
1446                Some(&deb822_lossless::IndentPattern::Fixed(1)),
1447                Some(BINARY_FIELD_ORDER),
1448            );
1449        } else {
1450            self.paragraph.remove("Description");
1451        }
1452    }
1453
1454    /// Return the upstream homepage
1455    pub fn homepage(&self) -> Option<url::Url> {
1456        self.paragraph.get("Homepage").and_then(|s| s.parse().ok())
1457    }
1458
1459    /// Set the upstream homepage
1460    pub fn set_homepage(&mut self, url: &url::Url) {
1461        self.set("Homepage", url.as_str());
1462    }
1463
1464    /// Set a field in the binary paragraph, using canonical field ordering for binary packages
1465    pub fn set(&mut self, key: &str, value: &str) {
1466        self.paragraph
1467            .set_with_field_order(key, value, BINARY_FIELD_ORDER);
1468    }
1469
1470    /// Retrieve a field
1471    pub fn get(&self, key: &str) -> Option<String> {
1472        self.paragraph.get(key)
1473    }
1474
1475    /// Check if this binary paragraph's range overlaps with the given range
1476    ///
1477    /// # Arguments
1478    /// * `range` - The text range to check for overlap
1479    ///
1480    /// # Returns
1481    /// `true` if the paragraph overlaps with the given range, `false` otherwise
1482    pub fn overlaps_range(&self, range: TextRange) -> bool {
1483        let para_range = self.paragraph.syntax().text_range();
1484        para_range.start() < range.end() && range.start() < para_range.end()
1485    }
1486
1487    /// Get fields in this binary paragraph that overlap with the given range
1488    ///
1489    /// # Arguments
1490    /// * `range` - The text range to check for overlaps
1491    ///
1492    /// # Returns
1493    /// An iterator over Entry items that overlap with the given range
1494    pub fn fields_in_range(
1495        &self,
1496        range: TextRange,
1497    ) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
1498        self.paragraph.entries().filter(move |entry| {
1499            let entry_range = entry.syntax().text_range();
1500            entry_range.start() < range.end() && range.start() < entry_range.end()
1501        })
1502    }
1503}
1504
1505#[cfg(test)]
1506mod tests {
1507    use super::*;
1508    use crate::relations::VersionConstraint;
1509
1510    #[test]
1511    fn test_source_set_field_ordering() {
1512        let mut control = Control::new();
1513        let mut source = control.add_source("mypackage");
1514
1515        // Add fields in random order
1516        source.set("Homepage", "https://example.com");
1517        source.set("Build-Depends", "debhelper");
1518        source.set("Standards-Version", "4.5.0");
1519        source.set("Maintainer", "Test <test@example.com>");
1520
1521        // Convert to string and check field order
1522        let output = source.to_string();
1523        let lines: Vec<&str> = output.lines().collect();
1524
1525        // Source should be first
1526        assert!(lines[0].starts_with("Source:"));
1527
1528        // Find the positions of each field
1529        let maintainer_pos = lines
1530            .iter()
1531            .position(|l| l.starts_with("Maintainer:"))
1532            .unwrap();
1533        let build_depends_pos = lines
1534            .iter()
1535            .position(|l| l.starts_with("Build-Depends:"))
1536            .unwrap();
1537        let standards_pos = lines
1538            .iter()
1539            .position(|l| l.starts_with("Standards-Version:"))
1540            .unwrap();
1541        let homepage_pos = lines
1542            .iter()
1543            .position(|l| l.starts_with("Homepage:"))
1544            .unwrap();
1545
1546        // Check ordering according to SOURCE_FIELD_ORDER
1547        assert!(maintainer_pos < build_depends_pos);
1548        assert!(build_depends_pos < standards_pos);
1549        assert!(standards_pos < homepage_pos);
1550    }
1551
1552    #[test]
1553    fn test_binary_set_field_ordering() {
1554        let mut control = Control::new();
1555        let mut binary = control.add_binary("mypackage");
1556
1557        // Add fields in random order
1558        binary.set("Description", "A test package");
1559        binary.set("Architecture", "amd64");
1560        binary.set("Depends", "libc6");
1561        binary.set("Section", "utils");
1562
1563        // Convert to string and check field order
1564        let output = binary.to_string();
1565        let lines: Vec<&str> = output.lines().collect();
1566
1567        // Package should be first
1568        assert!(lines[0].starts_with("Package:"));
1569
1570        // Find the positions of each field
1571        let arch_pos = lines
1572            .iter()
1573            .position(|l| l.starts_with("Architecture:"))
1574            .unwrap();
1575        let section_pos = lines
1576            .iter()
1577            .position(|l| l.starts_with("Section:"))
1578            .unwrap();
1579        let depends_pos = lines
1580            .iter()
1581            .position(|l| l.starts_with("Depends:"))
1582            .unwrap();
1583        let desc_pos = lines
1584            .iter()
1585            .position(|l| l.starts_with("Description:"))
1586            .unwrap();
1587
1588        // Check ordering according to BINARY_FIELD_ORDER
1589        assert!(arch_pos < section_pos);
1590        assert!(section_pos < depends_pos);
1591        assert!(depends_pos < desc_pos);
1592    }
1593
1594    #[test]
1595    fn test_source_specific_set_methods_use_field_ordering() {
1596        let mut control = Control::new();
1597        let mut source = control.add_source("mypackage");
1598
1599        // Use specific set_* methods in random order
1600        source.set_homepage(&"https://example.com".parse().unwrap());
1601        source.set_maintainer("Test <test@example.com>");
1602        source.set_standards_version("4.5.0");
1603        source.set_vcs_git("https://github.com/example/repo");
1604
1605        // Convert to string and check field order
1606        let output = source.to_string();
1607        let lines: Vec<&str> = output.lines().collect();
1608
1609        // Find the positions of each field
1610        let source_pos = lines.iter().position(|l| l.starts_with("Source:")).unwrap();
1611        let maintainer_pos = lines
1612            .iter()
1613            .position(|l| l.starts_with("Maintainer:"))
1614            .unwrap();
1615        let standards_pos = lines
1616            .iter()
1617            .position(|l| l.starts_with("Standards-Version:"))
1618            .unwrap();
1619        let vcs_git_pos = lines
1620            .iter()
1621            .position(|l| l.starts_with("Vcs-Git:"))
1622            .unwrap();
1623        let homepage_pos = lines
1624            .iter()
1625            .position(|l| l.starts_with("Homepage:"))
1626            .unwrap();
1627
1628        // Check ordering according to SOURCE_FIELD_ORDER
1629        assert!(source_pos < maintainer_pos);
1630        assert!(maintainer_pos < standards_pos);
1631        assert!(standards_pos < vcs_git_pos);
1632        assert!(vcs_git_pos < homepage_pos);
1633    }
1634
1635    #[test]
1636    fn test_binary_specific_set_methods_use_field_ordering() {
1637        let mut control = Control::new();
1638        let mut binary = control.add_binary("mypackage");
1639
1640        // Use specific set_* methods in random order
1641        binary.set_description(Some("A test package"));
1642        binary.set_architecture(Some("amd64"));
1643        let depends = "libc6".parse().unwrap();
1644        binary.set_depends(Some(&depends));
1645        binary.set_section(Some("utils"));
1646        binary.set_priority(Some(Priority::Optional));
1647
1648        // Convert to string and check field order
1649        let output = binary.to_string();
1650        let lines: Vec<&str> = output.lines().collect();
1651
1652        // Find the positions of each field
1653        let package_pos = lines
1654            .iter()
1655            .position(|l| l.starts_with("Package:"))
1656            .unwrap();
1657        let arch_pos = lines
1658            .iter()
1659            .position(|l| l.starts_with("Architecture:"))
1660            .unwrap();
1661        let section_pos = lines
1662            .iter()
1663            .position(|l| l.starts_with("Section:"))
1664            .unwrap();
1665        let priority_pos = lines
1666            .iter()
1667            .position(|l| l.starts_with("Priority:"))
1668            .unwrap();
1669        let depends_pos = lines
1670            .iter()
1671            .position(|l| l.starts_with("Depends:"))
1672            .unwrap();
1673        let desc_pos = lines
1674            .iter()
1675            .position(|l| l.starts_with("Description:"))
1676            .unwrap();
1677
1678        // Check ordering according to BINARY_FIELD_ORDER
1679        assert!(package_pos < arch_pos);
1680        assert!(arch_pos < section_pos);
1681        assert!(section_pos < priority_pos);
1682        assert!(priority_pos < depends_pos);
1683        assert!(depends_pos < desc_pos);
1684    }
1685
1686    #[test]
1687    fn test_parse() {
1688        let control: Control = r#"Source: foo
1689Section: libs
1690Priority: optional
1691Build-Depends: bar (>= 1.0.0), baz (>= 1.0.0)
1692Homepage: https://example.com
1693
1694"#
1695        .parse()
1696        .unwrap();
1697        let source = control.source().unwrap();
1698
1699        assert_eq!(source.name(), Some("foo".to_owned()));
1700        assert_eq!(source.section(), Some("libs".to_owned()));
1701        assert_eq!(source.priority(), Some(super::Priority::Optional));
1702        assert_eq!(
1703            source.homepage(),
1704            Some("https://example.com".parse().unwrap())
1705        );
1706        let bd = source.build_depends().unwrap();
1707        let entries = bd.entries().collect::<Vec<_>>();
1708        assert_eq!(entries.len(), 2);
1709        let rel = entries[0].relations().collect::<Vec<_>>().pop().unwrap();
1710        assert_eq!(rel.name(), "bar");
1711        assert_eq!(
1712            rel.version(),
1713            Some((
1714                VersionConstraint::GreaterThanEqual,
1715                "1.0.0".parse().unwrap()
1716            ))
1717        );
1718        let rel = entries[1].relations().collect::<Vec<_>>().pop().unwrap();
1719        assert_eq!(rel.name(), "baz");
1720        assert_eq!(
1721            rel.version(),
1722            Some((
1723                VersionConstraint::GreaterThanEqual,
1724                "1.0.0".parse().unwrap()
1725            ))
1726        );
1727    }
1728
1729    #[test]
1730    fn test_description() {
1731        let control: Control = r#"Source: foo
1732
1733Package: foo
1734Description: this is the short description
1735 And the longer one
1736 .
1737 is on the next lines
1738"#
1739        .parse()
1740        .unwrap();
1741        let binary = control.binaries().next().unwrap();
1742        assert_eq!(
1743            binary.description(),
1744            Some(
1745                "this is the short description\nAnd the longer one\n.\nis on the next lines"
1746                    .to_owned()
1747            )
1748        );
1749    }
1750
1751    #[test]
1752    fn test_set_description_on_package_without_description() {
1753        let control: Control = r#"Source: foo
1754
1755Package: foo
1756Architecture: amd64
1757"#
1758        .parse()
1759        .unwrap();
1760        let mut binary = control.binaries().next().unwrap();
1761
1762        // Set description on a binary that doesn't have one
1763        binary.set_description(Some(
1764            "Short description\nLonger description\n.\nAnother line",
1765        ));
1766
1767        let output = binary.to_string();
1768
1769        // Check that the description was set
1770        assert_eq!(
1771            binary.description(),
1772            Some("Short description\nLonger description\n.\nAnother line".to_owned())
1773        );
1774
1775        // Verify the output format has exactly one space indent
1776        assert_eq!(
1777            output,
1778            "Package: foo\nArchitecture: amd64\nDescription: Short description\n Longer description\n .\n Another line\n"
1779        );
1780    }
1781
1782    #[test]
1783    fn test_as_mut_deb822() {
1784        let mut control = Control::new();
1785        let deb822 = control.as_mut_deb822();
1786        let mut p = deb822.add_paragraph();
1787        p.set("Source", "foo");
1788        assert_eq!(control.source().unwrap().name(), Some("foo".to_owned()));
1789    }
1790
1791    #[test]
1792    fn test_as_deb822() {
1793        let control = Control::new();
1794        let _deb822: &Deb822 = control.as_deb822();
1795    }
1796
1797    #[test]
1798    fn test_set_depends() {
1799        let mut control = Control::new();
1800        let mut binary = control.add_binary("foo");
1801        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1802        binary.set_depends(Some(&relations));
1803    }
1804
1805    #[test]
1806    fn test_wrap_and_sort() {
1807        let mut control: Control = r#"Package: blah
1808Section:     libs
1809
1810
1811
1812Package: foo
1813Description: this is a 
1814      bar
1815      blah
1816"#
1817        .parse()
1818        .unwrap();
1819        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), false, None);
1820        let expected = r#"Package: blah
1821Section: libs
1822
1823Package: foo
1824Description: this is a 
1825  bar
1826  blah
1827"#
1828        .to_owned();
1829        assert_eq!(control.to_string(), expected);
1830    }
1831
1832    #[test]
1833    fn test_wrap_and_sort_source() {
1834        let mut control: Control = r#"Source: blah
1835Depends: foo, bar   (<=  1.0.0)
1836
1837"#
1838        .parse()
1839        .unwrap();
1840        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), true, None);
1841        let expected = r#"Source: blah
1842Depends: bar (<= 1.0.0), foo
1843"#
1844        .to_owned();
1845        assert_eq!(control.to_string(), expected);
1846    }
1847
1848    #[test]
1849    fn test_source_wrap_and_sort() {
1850        let control: Control = r#"Source: blah
1851Build-Depends: foo, bar (>= 1.0.0)
1852
1853"#
1854        .parse()
1855        .unwrap();
1856        let mut source = control.source().unwrap();
1857        source.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), true, None);
1858        // The actual behavior - the method modifies the source in-place
1859        // but doesn't automatically affect the overall control structure
1860        // So we just test that the method executes without error
1861        assert!(source.build_depends().is_some());
1862    }
1863
1864    #[test]
1865    fn test_binary_set_breaks() {
1866        let mut control = Control::new();
1867        let mut binary = control.add_binary("foo");
1868        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1869        binary.set_breaks(Some(&relations));
1870        assert!(binary.breaks().is_some());
1871    }
1872
1873    #[test]
1874    fn test_binary_set_pre_depends() {
1875        let mut control = Control::new();
1876        let mut binary = control.add_binary("foo");
1877        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1878        binary.set_pre_depends(Some(&relations));
1879        assert!(binary.pre_depends().is_some());
1880    }
1881
1882    #[test]
1883    fn test_binary_set_provides() {
1884        let mut control = Control::new();
1885        let mut binary = control.add_binary("foo");
1886        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1887        binary.set_provides(Some(&relations));
1888        assert!(binary.provides().is_some());
1889    }
1890
1891    #[test]
1892    fn test_source_build_conflicts() {
1893        let control: Control = r#"Source: blah
1894Build-Conflicts: foo, bar (>= 1.0.0)
1895
1896"#
1897        .parse()
1898        .unwrap();
1899        let source = control.source().unwrap();
1900        let conflicts = source.build_conflicts();
1901        assert!(conflicts.is_some());
1902    }
1903
1904    #[test]
1905    fn test_source_vcs_svn() {
1906        let control: Control = r#"Source: blah
1907Vcs-Svn: https://example.com/svn/repo
1908
1909"#
1910        .parse()
1911        .unwrap();
1912        let source = control.source().unwrap();
1913        assert_eq!(
1914            source.vcs_svn(),
1915            Some("https://example.com/svn/repo".to_string())
1916        );
1917    }
1918
1919    #[test]
1920    fn test_control_from_conversion() {
1921        let deb822_data = r#"Source: test
1922Section: libs
1923
1924"#;
1925        let deb822: Deb822 = deb822_data.parse().unwrap();
1926        let control = Control::from(deb822);
1927        assert!(control.source().is_some());
1928    }
1929
1930    #[test]
1931    fn test_fields_in_range() {
1932        let control_text = r#"Source: test-package
1933Maintainer: Test User <test@example.com>
1934Build-Depends: debhelper (>= 12)
1935
1936Package: test-binary
1937Architecture: any
1938Depends: ${shlibs:Depends}
1939Description: Test package
1940 This is a test package
1941"#;
1942        let control: Control = control_text.parse().unwrap();
1943
1944        // Test range that covers only the Source field
1945        let source_start = 0;
1946        let source_end = "Source: test-package".len();
1947        let source_range = TextRange::new((source_start as u32).into(), (source_end as u32).into());
1948
1949        let fields: Vec<_> = control.fields_in_range(source_range).collect();
1950        assert_eq!(fields.len(), 1);
1951        assert_eq!(fields[0].key(), Some("Source".to_string()));
1952
1953        // Test range that covers multiple fields in source paragraph
1954        let maintainer_start = control_text.find("Maintainer:").unwrap();
1955        let build_depends_end = control_text
1956            .find("Build-Depends: debhelper (>= 12)")
1957            .unwrap()
1958            + "Build-Depends: debhelper (>= 12)".len();
1959        let multi_range = TextRange::new(
1960            (maintainer_start as u32).into(),
1961            (build_depends_end as u32).into(),
1962        );
1963
1964        let fields: Vec<_> = control.fields_in_range(multi_range).collect();
1965        assert_eq!(fields.len(), 2);
1966        assert_eq!(fields[0].key(), Some("Maintainer".to_string()));
1967        assert_eq!(fields[1].key(), Some("Build-Depends".to_string()));
1968
1969        // Test range that spans across paragraphs
1970        let cross_para_start = control_text.find("Build-Depends:").unwrap();
1971        let cross_para_end =
1972            control_text.find("Architecture: any").unwrap() + "Architecture: any".len();
1973        let cross_range = TextRange::new(
1974            (cross_para_start as u32).into(),
1975            (cross_para_end as u32).into(),
1976        );
1977
1978        let fields: Vec<_> = control.fields_in_range(cross_range).collect();
1979        assert_eq!(fields.len(), 3); // Build-Depends, Package, Architecture
1980        assert_eq!(fields[0].key(), Some("Build-Depends".to_string()));
1981        assert_eq!(fields[1].key(), Some("Package".to_string()));
1982        assert_eq!(fields[2].key(), Some("Architecture".to_string()));
1983
1984        // Test empty range (should return no fields)
1985        let empty_range = TextRange::new(1000.into(), 1001.into());
1986        let fields: Vec<_> = control.fields_in_range(empty_range).collect();
1987        assert_eq!(fields.len(), 0);
1988    }
1989
1990    #[test]
1991    fn test_source_overlaps_range() {
1992        let control_text = r#"Source: test-package
1993Maintainer: Test User <test@example.com>
1994
1995Package: test-binary
1996Architecture: any
1997"#;
1998        let control: Control = control_text.parse().unwrap();
1999        let source = control.source().unwrap();
2000
2001        // Test range that overlaps with source paragraph
2002        let overlap_range = TextRange::new(10.into(), 30.into());
2003        assert!(source.overlaps_range(overlap_range));
2004
2005        // Test range that doesn't overlap with source paragraph
2006        let binary_start = control_text.find("Package:").unwrap();
2007        let no_overlap_range = TextRange::new(
2008            (binary_start as u32).into(),
2009            ((binary_start + 20) as u32).into(),
2010        );
2011        assert!(!source.overlaps_range(no_overlap_range));
2012
2013        // Test range that starts before and ends within source paragraph
2014        let partial_overlap = TextRange::new(0.into(), 15.into());
2015        assert!(source.overlaps_range(partial_overlap));
2016    }
2017
2018    #[test]
2019    fn test_source_fields_in_range() {
2020        let control_text = r#"Source: test-package
2021Maintainer: Test User <test@example.com>
2022Build-Depends: debhelper (>= 12)
2023
2024Package: test-binary
2025"#;
2026        let control: Control = control_text.parse().unwrap();
2027        let source = control.source().unwrap();
2028
2029        // Test range covering Maintainer field
2030        let maintainer_start = control_text.find("Maintainer:").unwrap();
2031        let maintainer_end = maintainer_start + "Maintainer: Test User <test@example.com>".len();
2032        let maintainer_range = TextRange::new(
2033            (maintainer_start as u32).into(),
2034            (maintainer_end as u32).into(),
2035        );
2036
2037        let fields: Vec<_> = source.fields_in_range(maintainer_range).collect();
2038        assert_eq!(fields.len(), 1);
2039        assert_eq!(fields[0].key(), Some("Maintainer".to_string()));
2040
2041        // Test range covering multiple fields
2042        let all_source_range = TextRange::new(0.into(), 100.into());
2043        let fields: Vec<_> = source.fields_in_range(all_source_range).collect();
2044        assert_eq!(fields.len(), 3); // Source, Maintainer, Build-Depends
2045    }
2046
2047    #[test]
2048    fn test_binary_overlaps_range() {
2049        let control_text = r#"Source: test-package
2050
2051Package: test-binary
2052Architecture: any
2053Depends: ${shlibs:Depends}
2054"#;
2055        let control: Control = control_text.parse().unwrap();
2056        let binary = control.binaries().next().unwrap();
2057
2058        // Test range that overlaps with binary paragraph
2059        let package_start = control_text.find("Package:").unwrap();
2060        let overlap_range = TextRange::new(
2061            (package_start as u32).into(),
2062            ((package_start + 30) as u32).into(),
2063        );
2064        assert!(binary.overlaps_range(overlap_range));
2065
2066        // Test range before binary paragraph
2067        let no_overlap_range = TextRange::new(0.into(), 10.into());
2068        assert!(!binary.overlaps_range(no_overlap_range));
2069    }
2070
2071    #[test]
2072    fn test_binary_fields_in_range() {
2073        let control_text = r#"Source: test-package
2074
2075Package: test-binary
2076Architecture: any
2077Depends: ${shlibs:Depends}
2078Description: Test binary
2079 This is a test binary package
2080"#;
2081        let control: Control = control_text.parse().unwrap();
2082        let binary = control.binaries().next().unwrap();
2083
2084        // Test range covering Architecture and Depends
2085        let arch_start = control_text.find("Architecture:").unwrap();
2086        let depends_end = control_text.find("Depends: ${shlibs:Depends}").unwrap()
2087            + "Depends: ${shlibs:Depends}".len();
2088        let range = TextRange::new((arch_start as u32).into(), (depends_end as u32).into());
2089
2090        let fields: Vec<_> = binary.fields_in_range(range).collect();
2091        assert_eq!(fields.len(), 2);
2092        assert_eq!(fields[0].key(), Some("Architecture".to_string()));
2093        assert_eq!(fields[1].key(), Some("Depends".to_string()));
2094
2095        // Test partial overlap with Description field
2096        let desc_start = control_text.find("Description:").unwrap();
2097        let partial_range = TextRange::new(
2098            ((desc_start + 5) as u32).into(),
2099            ((desc_start + 15) as u32).into(),
2100        );
2101        let fields: Vec<_> = binary.fields_in_range(partial_range).collect();
2102        assert_eq!(fields.len(), 1);
2103        assert_eq!(fields[0].key(), Some("Description".to_string()));
2104    }
2105
2106    #[test]
2107    fn test_incremental_parsing_use_case() {
2108        // This test simulates a real LSP use case where only changed fields are processed
2109        let control_text = r#"Source: example
2110Maintainer: John Doe <john@example.com>
2111Standards-Version: 4.6.0
2112Build-Depends: debhelper-compat (= 13)
2113
2114Package: example-bin
2115Architecture: all
2116Depends: ${misc:Depends}
2117Description: Example package
2118 This is an example.
2119"#;
2120        let control: Control = control_text.parse().unwrap();
2121
2122        // Simulate a change to Standards-Version field
2123        let change_start = control_text.find("Standards-Version:").unwrap();
2124        let change_end = change_start + "Standards-Version: 4.6.0".len();
2125        let change_range = TextRange::new((change_start as u32).into(), (change_end as u32).into());
2126
2127        // Only process fields in the changed range
2128        let affected_fields: Vec<_> = control.fields_in_range(change_range).collect();
2129        assert_eq!(affected_fields.len(), 1);
2130        assert_eq!(
2131            affected_fields[0].key(),
2132            Some("Standards-Version".to_string())
2133        );
2134
2135        // Verify that we're not processing unrelated fields
2136        for entry in &affected_fields {
2137            let key = entry.key().unwrap();
2138            assert_ne!(key, "Maintainer");
2139            assert_ne!(key, "Build-Depends");
2140            assert_ne!(key, "Architecture");
2141        }
2142    }
2143
2144    #[test]
2145    fn test_positioned_parse_errors() {
2146        // Test case from the requirements document
2147        let input = "Invalid: field\nBroken field without colon";
2148        let parsed = Control::parse(input);
2149
2150        // Should have positioned errors accessible
2151        let positioned_errors = parsed.positioned_errors();
2152        assert!(
2153            !positioned_errors.is_empty(),
2154            "Should have positioned errors"
2155        );
2156
2157        // Test that we can access error properties
2158        for error in positioned_errors {
2159            let start_offset: u32 = error.range.start().into();
2160            let end_offset: u32 = error.range.end().into();
2161
2162            // Verify we have meaningful error messages
2163            assert!(!error.message.is_empty());
2164
2165            // Verify ranges are valid
2166            assert!(start_offset <= end_offset);
2167            assert!(end_offset <= input.len() as u32);
2168
2169            // Error should have a code
2170            assert!(error.code.is_some());
2171
2172            println!(
2173                "Error at {:?}: {} (code: {:?})",
2174                error.range, error.message, error.code
2175            );
2176        }
2177
2178        // Should also be able to get string errors for backward compatibility
2179        let string_errors = parsed.errors();
2180        assert!(!string_errors.is_empty());
2181        assert_eq!(string_errors.len(), positioned_errors.len());
2182    }
2183
2184    #[test]
2185    fn test_sort_binaries_basic() {
2186        let input = r#"Source: foo
2187
2188Package: libfoo
2189Architecture: all
2190
2191Package: libbar
2192Architecture: all
2193"#;
2194
2195        let mut control: Control = input.parse().unwrap();
2196        control.sort_binaries(false);
2197
2198        let binaries: Vec<_> = control.binaries().collect();
2199        assert_eq!(binaries.len(), 2);
2200        assert_eq!(binaries[0].name(), Some("libbar".to_string()));
2201        assert_eq!(binaries[1].name(), Some("libfoo".to_string()));
2202    }
2203
2204    #[test]
2205    fn test_sort_binaries_keep_first() {
2206        let input = r#"Source: foo
2207
2208Package: zzz-first
2209Architecture: all
2210
2211Package: libbar
2212Architecture: all
2213
2214Package: libaaa
2215Architecture: all
2216"#;
2217
2218        let mut control: Control = input.parse().unwrap();
2219        control.sort_binaries(true);
2220
2221        let binaries: Vec<_> = control.binaries().collect();
2222        assert_eq!(binaries.len(), 3);
2223        // First binary should remain in place
2224        assert_eq!(binaries[0].name(), Some("zzz-first".to_string()));
2225        // The rest should be sorted
2226        assert_eq!(binaries[1].name(), Some("libaaa".to_string()));
2227        assert_eq!(binaries[2].name(), Some("libbar".to_string()));
2228    }
2229
2230    #[test]
2231    fn test_sort_binaries_already_sorted() {
2232        let input = r#"Source: foo
2233
2234Package: aaa
2235Architecture: all
2236
2237Package: bbb
2238Architecture: all
2239
2240Package: ccc
2241Architecture: all
2242"#;
2243
2244        let mut control: Control = input.parse().unwrap();
2245        control.sort_binaries(false);
2246
2247        let binaries: Vec<_> = control.binaries().collect();
2248        assert_eq!(binaries.len(), 3);
2249        assert_eq!(binaries[0].name(), Some("aaa".to_string()));
2250        assert_eq!(binaries[1].name(), Some("bbb".to_string()));
2251        assert_eq!(binaries[2].name(), Some("ccc".to_string()));
2252    }
2253
2254    #[test]
2255    fn test_sort_binaries_no_binaries() {
2256        let input = r#"Source: foo
2257Maintainer: test@example.com
2258"#;
2259
2260        let mut control: Control = input.parse().unwrap();
2261        control.sort_binaries(false);
2262
2263        // Should not crash, just do nothing
2264        assert_eq!(control.binaries().count(), 0);
2265    }
2266
2267    #[test]
2268    fn test_sort_binaries_one_binary() {
2269        let input = r#"Source: foo
2270
2271Package: bar
2272Architecture: all
2273"#;
2274
2275        let mut control: Control = input.parse().unwrap();
2276        control.sort_binaries(false);
2277
2278        let binaries: Vec<_> = control.binaries().collect();
2279        assert_eq!(binaries.len(), 1);
2280        assert_eq!(binaries[0].name(), Some("bar".to_string()));
2281    }
2282
2283    #[test]
2284    fn test_sort_binaries_preserves_fields() {
2285        let input = r#"Source: foo
2286
2287Package: zzz
2288Architecture: any
2289Depends: libc6
2290Description: ZZZ package
2291
2292Package: aaa
2293Architecture: all
2294Depends: ${misc:Depends}
2295Description: AAA package
2296"#;
2297
2298        let mut control: Control = input.parse().unwrap();
2299        control.sort_binaries(false);
2300
2301        let binaries: Vec<_> = control.binaries().collect();
2302        assert_eq!(binaries.len(), 2);
2303
2304        // First binary should be aaa
2305        assert_eq!(binaries[0].name(), Some("aaa".to_string()));
2306        assert_eq!(binaries[0].architecture(), Some("all".to_string()));
2307        assert_eq!(binaries[0].description(), Some("AAA package".to_string()));
2308
2309        // Second binary should be zzz
2310        assert_eq!(binaries[1].name(), Some("zzz".to_string()));
2311        assert_eq!(binaries[1].architecture(), Some("any".to_string()));
2312        assert_eq!(binaries[1].description(), Some("ZZZ package".to_string()));
2313    }
2314
2315    #[test]
2316    fn test_remove_binary_basic() {
2317        let mut control = Control::new();
2318        control.add_binary("foo");
2319        assert_eq!(control.binaries().count(), 1);
2320        assert!(control.remove_binary("foo"));
2321        assert_eq!(control.binaries().count(), 0);
2322    }
2323
2324    #[test]
2325    fn test_remove_binary_nonexistent() {
2326        let mut control = Control::new();
2327        control.add_binary("foo");
2328        assert!(!control.remove_binary("bar"));
2329        assert_eq!(control.binaries().count(), 1);
2330    }
2331
2332    #[test]
2333    fn test_remove_binary_multiple() {
2334        let mut control = Control::new();
2335        control.add_binary("foo");
2336        control.add_binary("bar");
2337        control.add_binary("baz");
2338        assert_eq!(control.binaries().count(), 3);
2339
2340        assert!(control.remove_binary("bar"));
2341        assert_eq!(control.binaries().count(), 2);
2342
2343        let names: Vec<_> = control.binaries().map(|b| b.name().unwrap()).collect();
2344        assert_eq!(names, vec!["foo", "baz"]);
2345    }
2346
2347    #[test]
2348    fn test_remove_binary_preserves_source() {
2349        let input = r#"Source: mypackage
2350
2351Package: foo
2352Architecture: all
2353
2354Package: bar
2355Architecture: all
2356"#;
2357        let mut control: Control = input.parse().unwrap();
2358        assert!(control.source().is_some());
2359        assert_eq!(control.binaries().count(), 2);
2360
2361        assert!(control.remove_binary("foo"));
2362
2363        // Source should still be present
2364        assert!(control.source().is_some());
2365        assert_eq!(
2366            control.source().unwrap().name(),
2367            Some("mypackage".to_string())
2368        );
2369
2370        // Only bar should remain
2371        assert_eq!(control.binaries().count(), 1);
2372        assert_eq!(
2373            control.binaries().next().unwrap().name(),
2374            Some("bar".to_string())
2375        );
2376    }
2377
2378    #[test]
2379    fn test_remove_binary_from_parsed() {
2380        let input = r#"Source: test
2381
2382Package: test-bin
2383Architecture: any
2384Depends: libc6
2385Description: Test binary
2386
2387Package: test-lib
2388Architecture: all
2389Description: Test library
2390"#;
2391        let mut control: Control = input.parse().unwrap();
2392        assert_eq!(control.binaries().count(), 2);
2393
2394        assert!(control.remove_binary("test-bin"));
2395
2396        let output = control.to_string();
2397        assert!(!output.contains("test-bin"));
2398        assert!(output.contains("test-lib"));
2399        assert!(output.contains("Source: test"));
2400    }
2401
2402    #[test]
2403    fn test_build_depends_preserves_indentation_after_removal() {
2404        let input = r#"Source: acpi-support
2405Section: admin
2406Priority: optional
2407Maintainer: Debian Acpi Team <pkg-acpi-devel@lists.alioth.debian.org>
2408Build-Depends: debhelper (>= 10), quilt (>= 0.40),
2409    libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config
2410"#;
2411        let control: Control = input.parse().unwrap();
2412        let mut source = control.source().unwrap();
2413
2414        // Get the Build-Depends
2415        let mut build_depends = source.build_depends().unwrap();
2416
2417        // Find and remove dh-systemd entry
2418        let mut to_remove = Vec::new();
2419        for (idx, entry) in build_depends.entries().enumerate() {
2420            for relation in entry.relations() {
2421                if relation.name() == "dh-systemd" {
2422                    to_remove.push(idx);
2423                    break;
2424                }
2425            }
2426        }
2427
2428        for idx in to_remove.into_iter().rev() {
2429            build_depends.remove_entry(idx);
2430        }
2431
2432        // Set it back
2433        source.set_build_depends(&build_depends);
2434
2435        let output = source.to_string();
2436
2437        // The indentation should be preserved (4 spaces on the continuation line)
2438        assert!(
2439            output.contains("Build-Depends: debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], pkg-config"),
2440            "Expected 4-space indentation to be preserved, but got:\n{}",
2441            output
2442        );
2443    }
2444
2445    #[test]
2446    fn test_build_depends_direct_string_set_loses_indentation() {
2447        let input = r#"Source: acpi-support
2448Section: admin
2449Priority: optional
2450Maintainer: Debian Acpi Team <pkg-acpi-devel@lists.alioth.debian.org>
2451Build-Depends: debhelper (>= 10), quilt (>= 0.40),
2452    libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config
2453"#;
2454        let control: Control = input.parse().unwrap();
2455        let mut source = control.source().unwrap();
2456
2457        // Get the Build-Depends as Relations
2458        let mut build_depends = source.build_depends().unwrap();
2459
2460        // Find and remove dh-systemd entry
2461        let mut to_remove = Vec::new();
2462        for (idx, entry) in build_depends.entries().enumerate() {
2463            for relation in entry.relations() {
2464                if relation.name() == "dh-systemd" {
2465                    to_remove.push(idx);
2466                    break;
2467                }
2468            }
2469        }
2470
2471        for idx in to_remove.into_iter().rev() {
2472            build_depends.remove_entry(idx);
2473        }
2474
2475        // Set it back using the string representation - this is what might cause the bug
2476        source.set("Build-Depends", &build_depends.to_string());
2477
2478        let output = source.to_string();
2479        println!("Output with string set:");
2480        println!("{}", output);
2481
2482        // Check if indentation is preserved
2483        // This test documents the current behavior - it may fail if indentation is lost
2484        assert!(
2485            output.contains("Build-Depends: debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], pkg-config"),
2486            "Expected 4-space indentation to be preserved, but got:\n{}",
2487            output
2488        );
2489    }
2490
2491    #[test]
2492    fn test_parse_mode_strict_default() {
2493        let control = Control::new();
2494        assert_eq!(control.parse_mode(), ParseMode::Strict);
2495
2496        let control: Control = "Source: test\n".parse().unwrap();
2497        assert_eq!(control.parse_mode(), ParseMode::Strict);
2498    }
2499
2500    #[test]
2501    fn test_parse_mode_new_with_mode() {
2502        let control_relaxed = Control::new_with_mode(ParseMode::Relaxed);
2503        assert_eq!(control_relaxed.parse_mode(), ParseMode::Relaxed);
2504
2505        let control_substvar = Control::new_with_mode(ParseMode::Substvar);
2506        assert_eq!(control_substvar.parse_mode(), ParseMode::Substvar);
2507    }
2508
2509    #[test]
2510    fn test_relaxed_mode_handles_broken_relations() {
2511        let input = r#"Source: test-package
2512Build-Depends: debhelper, @@@broken@@@, python3
2513
2514Package: test-pkg
2515Depends: libfoo, %%%invalid%%%, libbar
2516"#;
2517
2518        let (control, _errors) = Control::read_relaxed(input.as_bytes()).unwrap();
2519        assert_eq!(control.parse_mode(), ParseMode::Relaxed);
2520
2521        // These should not panic even with broken syntax
2522        if let Some(source) = control.source() {
2523            let bd = source.build_depends();
2524            assert!(bd.is_some());
2525            let relations = bd.unwrap();
2526            // Should have parsed the valid parts in relaxed mode
2527            assert!(relations.len() >= 2); // at least debhelper and python3
2528        }
2529
2530        for binary in control.binaries() {
2531            let deps = binary.depends();
2532            assert!(deps.is_some());
2533            let relations = deps.unwrap();
2534            // Should have parsed the valid parts
2535            assert!(relations.len() >= 2); // at least libfoo and libbar
2536        }
2537    }
2538
2539    #[test]
2540    fn test_substvar_mode_via_parse() {
2541        // Parse normally to get valid structure, but then we'd need substvar mode
2542        // Actually, we can't test this properly without the ability to set mode on parsed content
2543        // So let's just test that read_relaxed with substvars works
2544        let input = r#"Source: test-package
2545Build-Depends: debhelper, ${misc:Depends}
2546
2547Package: test-pkg
2548Depends: ${shlibs:Depends}, libfoo
2549"#;
2550
2551        // This will parse in relaxed mode, which also allows substvars to some degree
2552        let (control, _errors) = Control::read_relaxed(input.as_bytes()).unwrap();
2553
2554        if let Some(source) = control.source() {
2555            // Should parse without panic even with substvars
2556            let bd = source.build_depends();
2557            assert!(bd.is_some());
2558        }
2559
2560        for binary in control.binaries() {
2561            let deps = binary.depends();
2562            assert!(deps.is_some());
2563        }
2564    }
2565
2566    #[test]
2567    #[should_panic]
2568    fn test_strict_mode_panics_on_broken_syntax() {
2569        let input = r#"Source: test-package
2570Build-Depends: debhelper, @@@broken@@@
2571"#;
2572
2573        // Strict mode (default) should panic on invalid syntax
2574        let control: Control = input.parse().unwrap();
2575
2576        if let Some(source) = control.source() {
2577            // This should panic when trying to parse the broken Build-Depends
2578            let _ = source.build_depends();
2579        }
2580    }
2581
2582    #[test]
2583    fn test_from_file_relaxed_sets_relaxed_mode() {
2584        let input = r#"Source: test-package
2585Maintainer: Test <test@example.com>
2586"#;
2587
2588        let (control, _errors) = Control::read_relaxed(input.as_bytes()).unwrap();
2589        assert_eq!(control.parse_mode(), ParseMode::Relaxed);
2590    }
2591
2592    #[test]
2593    fn test_parse_mode_propagates_to_paragraphs() {
2594        let input = r#"Source: test-package
2595Build-Depends: debhelper, @@@invalid@@@, python3
2596
2597Package: test-pkg
2598Depends: libfoo, %%%bad%%%, libbar
2599"#;
2600
2601        // Parse in relaxed mode
2602        let (control, _) = Control::read_relaxed(input.as_bytes()).unwrap();
2603
2604        // The source and binary paragraphs should inherit relaxed mode
2605        // and not panic when parsing relations
2606        if let Some(source) = control.source() {
2607            assert!(source.build_depends().is_some());
2608        }
2609
2610        for binary in control.binaries() {
2611            assert!(binary.depends().is_some());
2612        }
2613    }
2614
2615    #[test]
2616    fn test_preserves_final_newline() {
2617        // Test that the final newline is preserved when writing control files
2618        let input_with_newline = "Source: test-package\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any\n";
2619        let control: Control = input_with_newline.parse().unwrap();
2620        let output = control.to_string();
2621        assert_eq!(output, input_with_newline);
2622    }
2623
2624    #[test]
2625    fn test_preserves_no_final_newline() {
2626        // Test that absence of final newline is also preserved (even though it's not POSIX-compliant)
2627        let input_without_newline = "Source: test-package\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any";
2628        let control: Control = input_without_newline.parse().unwrap();
2629        let output = control.to_string();
2630        assert_eq!(output, input_without_newline);
2631    }
2632
2633    #[test]
2634    fn test_final_newline_after_modifications() {
2635        // Test that final newline is preserved even after modifications
2636        let input = "Source: test-package\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any\n";
2637        let control: Control = input.parse().unwrap();
2638
2639        // Make a modification
2640        let mut source = control.source().unwrap();
2641        source.set_section(Some("utils"));
2642
2643        let output = control.to_string();
2644        let expected = "Source: test-package\nSection: utils\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any\n";
2645        assert_eq!(output, expected);
2646    }
2647
2648    #[test]
2649    fn test_source_in_range() {
2650        // Test that source_in_range() returns the source when it intersects with range
2651        let input = r#"Source: test-package
2652Maintainer: Test <test@example.com>
2653Section: utils
2654
2655Package: test-pkg
2656Architecture: any
2657"#;
2658        let control: Control = input.parse().unwrap();
2659
2660        // Get the text range of the source paragraph
2661        let source = control.source().unwrap();
2662        let source_range = source.as_deb822().text_range();
2663
2664        // Query with the exact range - should return the source
2665        let result = control.source_in_range(source_range);
2666        assert!(result.is_some());
2667        assert_eq!(result.unwrap().name(), Some("test-package".to_string()));
2668
2669        // Query with a range that overlaps the source
2670        let overlap_range = TextRange::new(0.into(), 20.into());
2671        let result = control.source_in_range(overlap_range);
2672        assert!(result.is_some());
2673        assert_eq!(result.unwrap().name(), Some("test-package".to_string()));
2674
2675        // Query with a range that doesn't overlap the source
2676        let no_overlap_range = TextRange::new(100.into(), 150.into());
2677        let result = control.source_in_range(no_overlap_range);
2678        assert!(result.is_none());
2679    }
2680
2681    #[test]
2682    fn test_binaries_in_range_single() {
2683        // Test that binaries_in_range() returns a single binary in range
2684        let input = r#"Source: test-package
2685Maintainer: Test <test@example.com>
2686
2687Package: test-pkg
2688Architecture: any
2689
2690Package: another-pkg
2691Architecture: all
2692"#;
2693        let control: Control = input.parse().unwrap();
2694
2695        // Get the text range of the first binary paragraph
2696        let first_binary = control.binaries().next().unwrap();
2697        let binary_range = first_binary.as_deb822().text_range();
2698
2699        // Query with that range - should return only the first binary
2700        let binaries: Vec<_> = control.binaries_in_range(binary_range).collect();
2701        assert_eq!(binaries.len(), 1);
2702        assert_eq!(binaries[0].name(), Some("test-pkg".to_string()));
2703    }
2704
2705    #[test]
2706    fn test_binaries_in_range_multiple() {
2707        // Test that binaries_in_range() returns multiple binaries in range
2708        let input = r#"Source: test-package
2709Maintainer: Test <test@example.com>
2710
2711Package: test-pkg
2712Architecture: any
2713
2714Package: another-pkg
2715Architecture: all
2716
2717Package: third-pkg
2718Architecture: any
2719"#;
2720        let control: Control = input.parse().unwrap();
2721
2722        // Create a range that covers the first two binary paragraphs
2723        let range = TextRange::new(50.into(), 130.into());
2724
2725        // Query with that range
2726        let binaries: Vec<_> = control.binaries_in_range(range).collect();
2727        assert!(binaries.len() >= 2);
2728        assert!(binaries
2729            .iter()
2730            .any(|b| b.name() == Some("test-pkg".to_string())));
2731        assert!(binaries
2732            .iter()
2733            .any(|b| b.name() == Some("another-pkg".to_string())));
2734    }
2735
2736    #[test]
2737    fn test_binaries_in_range_none() {
2738        // Test that binaries_in_range() returns empty iterator when no binaries in range
2739        let input = r#"Source: test-package
2740Maintainer: Test <test@example.com>
2741
2742Package: test-pkg
2743Architecture: any
2744"#;
2745        let control: Control = input.parse().unwrap();
2746
2747        // Create a range that's way beyond the document
2748        let range = TextRange::new(1000.into(), 2000.into());
2749
2750        // Should return empty iterator
2751        let binaries: Vec<_> = control.binaries_in_range(range).collect();
2752        assert_eq!(binaries.len(), 0);
2753    }
2754
2755    #[test]
2756    fn test_binaries_in_range_all() {
2757        // Test that binaries_in_range() returns all binaries when range covers entire document
2758        let input = r#"Source: test-package
2759Maintainer: Test <test@example.com>
2760
2761Package: test-pkg
2762Architecture: any
2763
2764Package: another-pkg
2765Architecture: all
2766"#;
2767        let control: Control = input.parse().unwrap();
2768
2769        // Create a range that covers the entire document
2770        let range = TextRange::new(0.into(), input.len().try_into().unwrap());
2771
2772        // Should return all binaries
2773        let binaries: Vec<_> = control.binaries_in_range(range).collect();
2774        assert_eq!(binaries.len(), 2);
2775    }
2776
2777    #[test]
2778    fn test_source_in_range_partial_overlap() {
2779        // Test that source_in_range() returns source with partial overlap
2780        let input = r#"Source: test-package
2781Maintainer: Test <test@example.com>
2782
2783Package: test-pkg
2784Architecture: any
2785"#;
2786        let control: Control = input.parse().unwrap();
2787
2788        // Create a range that starts in the middle of the source paragraph
2789        let range = TextRange::new(10.into(), 30.into());
2790
2791        // Should include the source since it overlaps
2792        let result = control.source_in_range(range);
2793        assert!(result.is_some());
2794        assert_eq!(result.unwrap().name(), Some("test-package".to_string()));
2795    }
2796
2797    #[test]
2798    fn test_wrap_and_sort_long_build_depends_wraps_to_one_per_line() {
2799        // A Build-Depends value that, on a single line, far exceeds the
2800        // requested max_line_length_one_liner must be broken into one
2801        // relation per line — matching `wrap-and-sort` behaviour.
2802        let input = r#"Source: test-package
2803Maintainer: Test <test@example.com>
2804Build-Depends: debhelper-compat (= 13), aaaa, bbbb, cccc, dddd, eeee, ffff, gggg, hhhh, iiii, jjjj
2805
2806"#;
2807        let mut control: Control = input.parse().unwrap();
2808        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(1), false, Some(79));
2809
2810        let expected = r#"Source: test-package
2811Maintainer: Test <test@example.com>
2812Build-Depends: aaaa,
2813 bbbb,
2814 cccc,
2815 dddd,
2816 debhelper-compat (= 13),
2817 eeee,
2818 ffff,
2819 gggg,
2820 hhhh,
2821 iiii,
2822 jjjj
2823"#;
2824        assert_eq!(control.to_string(), expected);
2825    }
2826
2827    #[test]
2828    fn test_wrap_and_sort_short_build_depends_stays_one_line() {
2829        // A short Build-Depends value that fits within the line length
2830        // should remain on a single line.
2831        let input = r#"Source: test-package
2832Maintainer: Test <test@example.com>
2833Build-Depends: debhelper-compat (= 13), foo, bar
2834
2835"#;
2836        let mut control: Control = input.parse().unwrap();
2837        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(1), false, Some(79));
2838
2839        // Note: existing behaviour drops the space after the colon for the
2840        // one-liner branch in rebuild_value; that is unrelated to this fix.
2841        let expected = "Source: test-package\nMaintainer: Test <test@example.com>\nBuild-Depends:bar, debhelper-compat (= 13), foo\n";
2842        assert_eq!(control.to_string(), expected);
2843    }
2844
2845    #[test]
2846    fn test_wrap_and_sort_long_build_depends_keeps_brackets_intact() {
2847        // Each entry stays whole on its line — including the `(...)`,
2848        // `[...]` and `<...>` sections — because we emit by parsed entry
2849        // rather than splitting the formatted string on commas.
2850        // Note: a separate bug in Relation::wrap_and_sort drops the `!` from
2851        // architecture restrictions, so we use a plain `[amd64 arm64]` here.
2852        let value = "foo (>= 1.0), bar [amd64 arm64], baz <stage1 !nocheck>, qux, quux, corge, grault, garply, waldo, fred";
2853        let input = format!(
2854            "Source: test-package\nMaintainer: Test <test@example.com>\nBuild-Depends: {}\n\n",
2855            value
2856        );
2857        let mut control: Control = input.parse().unwrap();
2858        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(1), false, Some(79));
2859        let out = control.to_string();
2860        assert!(out.contains("bar [amd64 arm64],\n"), "out was: {}", out);
2861        assert!(
2862            out.contains(" baz <stage1 !nocheck>,\n"),
2863            "out was: {}",
2864            out
2865        );
2866        assert!(out.contains(" foo (>= 1.0),\n"), "out was: {}", out);
2867    }
2868
2869    #[test]
2870    fn test_wrap_and_sort_with_malformed_relations() {
2871        // Test that wrap_and_sort doesn't panic on malformed relations
2872        // and preserves the original value when parsing fails
2873        let input = r#"Source: test-package
2874Maintainer: Test <test@example.com>
2875Build-Depends: some invalid relation syntax here
2876
2877Package: test-pkg
2878Architecture: any
2879"#;
2880        let mut control: Control = input.parse().unwrap();
2881
2882        // This should not panic, even with malformed relations
2883        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), false, None);
2884
2885        // The malformed field should be preserved as-is (lossless behavior)
2886        let output = control.to_string();
2887        let expected = r#"Source: test-package
2888Maintainer: Test <test@example.com>
2889Build-Depends: some invalid relation syntax here
2890
2891Package: test-pkg
2892Architecture: any
2893"#;
2894        assert_eq!(output, expected);
2895    }
2896}