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