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