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