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