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