Skip to main content

debian_control/lossless/
control.rs

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