debian_control/lossless/
control.rs

1//! This module provides a lossless representation of a Debian control file.
2//!
3//! # Example
4//! ```rust
5//! use debian_control::lossless::Control;
6//! use debian_control::relations::VersionConstraint;
7//! let input = r###"Source: dulwich
8//! ## Comments are preserved
9//! Maintainer: Jelmer Vernooij <jelmer@jelmer.uk>
10//! Build-Depends: python3, debhelper-compat (= 12)
11//!
12//! Package: python3-dulwich
13//! Architecture: amd64
14//! Description: Pure-python git implementation
15//! "###;
16//!
17//! let mut control: Control = input.parse().unwrap();
18//!
19//! // Bump debhelper-compat
20//! let source = control.source().unwrap();
21//! let bd = source.build_depends().unwrap();
22//!
23//! // Get entry with index 1 in Build-Depends, then set the version
24//! let entry = bd.get_entry(1).unwrap();
25//! let mut debhelper = entry.relations().next().unwrap();
26//! assert_eq!(debhelper.name(), "debhelper-compat");
27//! debhelper.set_version(Some((VersionConstraint::Equal, "13".parse().unwrap())));
28//!
29//! assert_eq!(source.to_string(), r###"Source: dulwich
30//! ## Comments are preserved
31//! Maintainer: Jelmer Vernooij <jelmer@jelmer.uk>
32//! Build-Depends: python3, debhelper-compat (= 12)
33//! "###);
34//! ```
35use crate::fields::{MultiArch, Priority};
36use crate::lossless::relations::Relations;
37use deb822_lossless::{Deb822, Paragraph};
38use rowan::ast::AstNode;
39
40fn format_field(name: &str, value: &str) -> String {
41    match name {
42        "Uploaders" => value
43            .split(',')
44            .map(|s| s.trim().to_string())
45            .collect::<Vec<_>>()
46            .join(",\n"),
47        "Build-Depends"
48        | "Build-Depends-Indep"
49        | "Build-Depends-Arch"
50        | "Build-Conflicts"
51        | "Build-Conflicts-Indep"
52        | "Build-Conflics-Arch"
53        | "Depends"
54        | "Recommends"
55        | "Suggests"
56        | "Enhances"
57        | "Pre-Depends"
58        | "Breaks" => {
59            let relations: Relations = value.parse().unwrap();
60            let relations = relations.wrap_and_sort();
61            relations.to_string()
62        }
63        _ => value.to_string(),
64    }
65}
66
67/// A Debian control file
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Control(Deb822);
70
71impl Control {
72    /// Create a new control file
73    pub fn new() -> Self {
74        Control(Deb822::new())
75    }
76
77    /// Return the underlying deb822 object, mutable
78    pub fn as_mut_deb822(&mut self) -> &mut Deb822 {
79        &mut self.0
80    }
81
82    /// Return the underlying deb822 object
83    pub fn as_deb822(&self) -> &Deb822 {
84        &self.0
85    }
86
87    /// Parse control file text, returning a Parse result
88    pub fn parse(text: &str) -> deb822_lossless::Parse<Control> {
89        let deb822_parse = Deb822::parse(text);
90        // Transform Parse<Deb822> to Parse<Control>
91        let green = deb822_parse.green().clone();
92        let errors = deb822_parse.errors().to_vec();
93        let positioned_errors = deb822_parse.positioned_errors().to_vec();
94        deb822_lossless::Parse::new_with_positioned_errors(green, errors, positioned_errors)
95    }
96
97    /// Return the source package
98    pub fn source(&self) -> Option<Source> {
99        self.0
100            .paragraphs()
101            .find(|p| p.get("Source").is_some())
102            .map(Source)
103    }
104
105    /// Iterate over all binary packages
106    pub fn binaries(&self) -> impl Iterator<Item = Binary> {
107        self.0
108            .paragraphs()
109            .filter(|p| p.get("Package").is_some())
110            .map(Binary)
111    }
112
113    /// Add a new source package
114    ///
115    /// # Arguments
116    /// * `name` - The name of the source package
117    ///
118    /// # Returns
119    /// The newly created source package
120    ///
121    /// # Example
122    /// ```rust
123    /// use debian_control::lossless::control::Control;
124    /// let mut control = Control::new();
125    /// let source = control.add_source("foo");
126    /// assert_eq!(source.name(), Some("foo".to_owned()));
127    /// ```
128    pub fn add_source(&mut self, name: &str) -> Source {
129        let mut p = self.0.add_paragraph();
130        p.set("Source", name);
131        self.source().unwrap()
132    }
133
134    /// Add new binary package
135    ///
136    /// # Arguments
137    /// * `name` - The name of the binary package
138    ///
139    /// # Returns
140    /// The newly created binary package
141    ///
142    /// # Example
143    /// ```rust
144    /// use debian_control::lossless::control::Control;
145    /// let mut control = Control::new();
146    /// let binary = control.add_binary("foo");
147    /// assert_eq!(binary.name(), Some("foo".to_owned()));
148    /// ```
149    pub fn add_binary(&mut self, name: &str) -> Binary {
150        let mut p = self.0.add_paragraph();
151        p.set("Package", name);
152        Binary(p)
153    }
154
155    /// Read a control file from a file
156    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, deb822_lossless::Error> {
157        Ok(Control(Deb822::from_file(path)?))
158    }
159
160    /// Read a control file from a file, allowing syntax errors
161    pub fn from_file_relaxed<P: AsRef<std::path::Path>>(
162        path: P,
163    ) -> Result<(Self, Vec<String>), std::io::Error> {
164        let (control, errors) = Deb822::from_file_relaxed(path)?;
165        Ok((Control(control), errors))
166    }
167
168    /// Read a control file from a reader
169    pub fn read<R: std::io::Read>(mut r: R) -> Result<Self, deb822_lossless::Error> {
170        Ok(Control(Deb822::read(&mut r)?))
171    }
172
173    /// Read a control file from a reader, allowing syntax errors
174    pub fn read_relaxed<R: std::io::Read>(
175        mut r: R,
176    ) -> Result<(Self, Vec<String>), deb822_lossless::Error> {
177        let (control, errors) = Deb822::read_relaxed(&mut r)?;
178        Ok((Self(control), errors))
179    }
180
181    /// Wrap and sort the control file
182    ///
183    /// # Arguments
184    /// * `indentation` - The indentation to use
185    /// * `immediate_empty_line` - Whether to add an empty line at the start of multi-line fields
186    /// * `max_line_length_one_liner` - The maximum line length for one-liner fields
187    pub fn wrap_and_sort(
188        &mut self,
189        indentation: deb822_lossless::Indentation,
190        immediate_empty_line: bool,
191        max_line_length_one_liner: Option<usize>,
192    ) {
193        let sort_paragraphs = |a: &Paragraph, b: &Paragraph| -> std::cmp::Ordering {
194            // Sort Source before Package
195            let a_is_source = a.get("Source").is_some();
196            let b_is_source = b.get("Source").is_some();
197
198            if a_is_source && !b_is_source {
199                return std::cmp::Ordering::Less;
200            } else if !a_is_source && b_is_source {
201                return std::cmp::Ordering::Greater;
202            } else if a_is_source && b_is_source {
203                return a.get("Source").cmp(&b.get("Source"));
204            }
205
206            a.get("Package").cmp(&b.get("Package"))
207        };
208
209        let wrap_paragraph = |p: &Paragraph| -> Paragraph {
210            // TODO: Add Source/Package specific wrapping
211            // TODO: Add support for wrapping and sorting fields
212            p.wrap_and_sort(
213                indentation,
214                immediate_empty_line,
215                max_line_length_one_liner,
216                None,
217                Some(&format_field),
218            )
219        };
220
221        self.0 = self
222            .0
223            .wrap_and_sort(Some(&sort_paragraphs), Some(&wrap_paragraph));
224    }
225
226    /// Iterate over fields that overlap with the given range
227    ///
228    /// This method returns all fields (entries) from all paragraphs that have any overlap
229    /// with the specified text range. This is useful for incremental parsing in LSP contexts
230    /// where you only want to process fields that were affected by a text change.
231    ///
232    /// # Arguments
233    /// * `range` - The text range to check for overlaps
234    ///
235    /// # Returns
236    /// An iterator over all Entry items that overlap with the given range
237    ///
238    /// # Example
239    /// ```rust
240    /// use debian_control::lossless::Control;
241    /// use deb822_lossless::TextRange;
242    ///
243    /// let control_text = "Source: foo\nMaintainer: test@example.com\n\nPackage: bar\nArchitecture: all\n";
244    /// let control: Control = control_text.parse().unwrap();
245    ///
246    /// // Get fields in a specific range (e.g., where a change occurred)
247    /// let change_range = TextRange::new(20.into(), 40.into());
248    /// for entry in control.fields_in_range(change_range) {
249    ///     if let Some(key) = entry.key() {
250    ///         println!("Field {} was in the changed range", key);
251    ///     }
252    /// }
253    /// ```
254    pub fn fields_in_range(
255        &self,
256        range: rowan::TextRange,
257    ) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
258        self.0
259            .paragraphs()
260            .flat_map(move |p| p.entries().collect::<Vec<_>>())
261            .filter(move |entry| {
262                let entry_range = entry.syntax().text_range();
263                // Check if ranges overlap
264                entry_range.start() < range.end() && range.start() < entry_range.end()
265            })
266    }
267}
268
269impl From<Control> for Deb822 {
270    fn from(c: Control) -> Self {
271        c.0
272    }
273}
274
275impl From<Deb822> for Control {
276    fn from(d: Deb822) -> Self {
277        Control(d)
278    }
279}
280
281impl Default for Control {
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287impl std::str::FromStr for Control {
288    type Err = deb822_lossless::ParseError;
289
290    fn from_str(s: &str) -> Result<Self, Self::Err> {
291        Control::parse(s).to_result()
292    }
293}
294
295/// A source package paragraph
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct Source(Paragraph);
298
299impl From<Source> for Paragraph {
300    fn from(s: Source) -> Self {
301        s.0
302    }
303}
304
305impl From<Paragraph> for Source {
306    fn from(p: Paragraph) -> Self {
307        Source(p)
308    }
309}
310
311impl std::fmt::Display for Source {
312    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
313        self.0.fmt(f)
314    }
315}
316
317impl Source {
318    /// The name of the source package.
319    pub fn name(&self) -> Option<String> {
320        self.0.get("Source")
321    }
322
323    /// Wrap and sort the control file paragraph
324    pub fn wrap_and_sort(
325        &mut self,
326        indentation: deb822_lossless::Indentation,
327        immediate_empty_line: bool,
328        max_line_length_one_liner: Option<usize>,
329    ) {
330        self.0 = self.0.wrap_and_sort(
331            indentation,
332            immediate_empty_line,
333            max_line_length_one_liner,
334            None,
335            Some(&format_field),
336        );
337    }
338
339    /// Return the underlying deb822 paragraph, mutable
340    pub fn as_mut_deb822(&mut self) -> &mut Paragraph {
341        &mut self.0
342    }
343
344    /// Return the underlying deb822 paragraph
345    pub fn as_deb822(&self) -> &Paragraph {
346        &self.0
347    }
348
349    /// Set the name of the source package.
350    pub fn set_name(&mut self, name: &str) {
351        self.0.set("Source", name);
352    }
353
354    /// The default section of the packages built from this source package.
355    pub fn section(&self) -> Option<String> {
356        self.0.get("Section")
357    }
358
359    /// Set the section of the source package
360    pub fn set_section(&mut self, section: Option<&str>) {
361        if let Some(section) = section {
362            self.0.set("Section", section);
363        } else {
364            self.0.remove("Section");
365        }
366    }
367
368    /// The default priority of the packages built from this source package.
369    pub fn priority(&self) -> Option<Priority> {
370        self.0.get("Priority").and_then(|v| v.parse().ok())
371    }
372
373    /// Set the priority of the source package
374    pub fn set_priority(&mut self, priority: Option<Priority>) {
375        if let Some(priority) = priority {
376            self.0.set("Priority", priority.to_string().as_str());
377        } else {
378            self.0.remove("Priority");
379        }
380    }
381
382    /// The maintainer of the package.
383    pub fn maintainer(&self) -> Option<String> {
384        self.0.get("Maintainer")
385    }
386
387    /// Set the maintainer of the package
388    pub fn set_maintainer(&mut self, maintainer: &str) {
389        self.0.set("Maintainer", maintainer);
390    }
391
392    /// The build dependencies of the package.
393    pub fn build_depends(&self) -> Option<Relations> {
394        self.0.get("Build-Depends").map(|s| s.parse().unwrap())
395    }
396
397    /// Set the Build-Depends field
398    pub fn set_build_depends(&mut self, relations: &Relations) {
399        self.0.set("Build-Depends", relations.to_string().as_str());
400    }
401
402    /// Return the Build-Depends-Indep field
403    pub fn build_depends_indep(&self) -> Option<Relations> {
404        self.0
405            .get("Build-Depends-Indep")
406            .map(|s| s.parse().unwrap())
407    }
408
409    /// Return the Build-Depends-Arch field
410    pub fn build_depends_arch(&self) -> Option<Relations> {
411        self.0.get("Build-Depends-Arch").map(|s| s.parse().unwrap())
412    }
413
414    /// The build conflicts of the package.
415    pub fn build_conflicts(&self) -> Option<Relations> {
416        self.0.get("Build-Conflicts").map(|s| s.parse().unwrap())
417    }
418
419    /// Return the Build-Conflicts-Indep field
420    pub fn build_conflicts_indep(&self) -> Option<Relations> {
421        self.0
422            .get("Build-Conflicts-Indep")
423            .map(|s| s.parse().unwrap())
424    }
425
426    /// Return the Build-Conflicts-Arch field
427    pub fn build_conflicts_arch(&self) -> Option<Relations> {
428        self.0
429            .get("Build-Conflicts-Arch")
430            .map(|s| s.parse().unwrap())
431    }
432
433    /// Return the standards version
434    pub fn standards_version(&self) -> Option<String> {
435        self.0.get("Standards-Version")
436    }
437
438    /// Set the Standards-Version field
439    pub fn set_standards_version(&mut self, version: &str) {
440        self.0.set("Standards-Version", version);
441    }
442
443    /// Return the upstrea mHomepage
444    pub fn homepage(&self) -> Option<url::Url> {
445        self.0.get("Homepage").and_then(|s| s.parse().ok())
446    }
447
448    /// Set the Homepage field
449    pub fn set_homepage(&mut self, homepage: &url::Url) {
450        self.0.set("Homepage", homepage.to_string().as_str());
451    }
452
453    /// Return the Vcs-Git field
454    pub fn vcs_git(&self) -> Option<String> {
455        self.0.get("Vcs-Git")
456    }
457
458    /// Set the Vcs-Git field
459    pub fn set_vcs_git(&mut self, url: &str) {
460        self.0.set("Vcs-Git", url);
461    }
462
463    /// Return the Vcs-Browser field
464    pub fn vcs_svn(&self) -> Option<String> {
465        self.0.get("Vcs-Svn").map(|s| s.to_string())
466    }
467
468    /// Set the Vcs-Svn field
469    pub fn set_vcs_svn(&mut self, url: &str) {
470        self.0.set("Vcs-Svn", url);
471    }
472
473    /// Return the Vcs-Bzr field
474    pub fn vcs_bzr(&self) -> Option<String> {
475        self.0.get("Vcs-Bzr").map(|s| s.to_string())
476    }
477
478    /// Set the Vcs-Bzr field
479    pub fn set_vcs_bzr(&mut self, url: &str) {
480        self.0.set("Vcs-Bzr", url);
481    }
482
483    /// Return the Vcs-Arch field
484    pub fn vcs_arch(&self) -> Option<String> {
485        self.0.get("Vcs-Arch").map(|s| s.to_string())
486    }
487
488    /// Set the Vcs-Arch field
489    pub fn set_vcs_arch(&mut self, url: &str) {
490        self.0.set("Vcs-Arch", url);
491    }
492
493    /// Return the Vcs-Svk field
494    pub fn vcs_svk(&self) -> Option<String> {
495        self.0.get("Vcs-Svk").map(|s| s.to_string())
496    }
497
498    /// Set the Vcs-Svk field
499    pub fn set_vcs_svk(&mut self, url: &str) {
500        self.0.set("Vcs-Svk", url);
501    }
502
503    /// Return the Vcs-Darcs field
504    pub fn vcs_darcs(&self) -> Option<String> {
505        self.0.get("Vcs-Darcs").map(|s| s.to_string())
506    }
507
508    /// Set the Vcs-Darcs field
509    pub fn set_vcs_darcs(&mut self, url: &str) {
510        self.0.set("Vcs-Darcs", url);
511    }
512
513    /// Return the Vcs-Mtn field
514    pub fn vcs_mtn(&self) -> Option<String> {
515        self.0.get("Vcs-Mtn").map(|s| s.to_string())
516    }
517
518    /// Set the Vcs-Mtn field
519    pub fn set_vcs_mtn(&mut self, url: &str) {
520        self.0.set("Vcs-Mtn", url);
521    }
522
523    /// Return the Vcs-Cvs field
524    pub fn vcs_cvs(&self) -> Option<String> {
525        self.0.get("Vcs-Cvs").map(|s| s.to_string())
526    }
527
528    /// Set the Vcs-Cvs field
529    pub fn set_vcs_cvs(&mut self, url: &str) {
530        self.0.set("Vcs-Cvs", url);
531    }
532
533    /// Return the Vcs-Hg field
534    pub fn vcs_hg(&self) -> Option<String> {
535        self.0.get("Vcs-Hg").map(|s| s.to_string())
536    }
537
538    /// Set the Vcs-Hg field
539    pub fn set_vcs_hg(&mut self, url: &str) {
540        self.0.set("Vcs-Hg", url);
541    }
542
543    /// Return the Vcs-Browser field
544    pub fn vcs_browser(&self) -> Option<String> {
545        self.0.get("Vcs-Browser")
546    }
547
548    /// Return the Vcs used by the package
549    pub fn vcs(&self) -> Option<crate::vcs::Vcs> {
550        for (name, value) in self.0.items() {
551            if name.starts_with("Vcs-") && name != "Vcs-Browser" {
552                return crate::vcs::Vcs::from_field(&name, &value).ok();
553            }
554        }
555        None
556    }
557
558    /// Set the Vcs-Browser field
559    pub fn set_vcs_browser(&mut self, url: Option<&str>) {
560        if let Some(url) = url {
561            self.0.set("Vcs-Browser", url);
562        } else {
563            self.0.remove("Vcs-Browser");
564        }
565    }
566
567    /// Return the Uploaders field
568    pub fn uploaders(&self) -> Option<Vec<String>> {
569        self.0
570            .get("Uploaders")
571            .map(|s| s.split(',').map(|s| s.trim().to_owned()).collect())
572    }
573
574    /// Set the uploaders field
575    pub fn set_uploaders(&mut self, uploaders: &[&str]) {
576        self.0.set(
577            "Uploaders",
578            uploaders
579                .iter()
580                .map(|s| s.to_string())
581                .collect::<Vec<_>>()
582                .join(", ")
583                .as_str(),
584        );
585    }
586
587    /// Return the architecture field
588    pub fn architecture(&self) -> Option<String> {
589        self.0.get("Architecture")
590    }
591
592    /// Set the architecture field
593    pub fn set_architecture(&mut self, arch: Option<&str>) {
594        if let Some(arch) = arch {
595            self.0.set("Architecture", arch);
596        } else {
597            self.0.remove("Architecture");
598        }
599    }
600
601    /// Return the Rules-Requires-Root field
602    pub fn rules_requires_root(&self) -> Option<bool> {
603        self.0
604            .get("Rules-Requires-Root")
605            .map(|s| match s.to_lowercase().as_str() {
606                "yes" => true,
607                "no" => false,
608                _ => panic!("invalid Rules-Requires-Root value"),
609            })
610    }
611
612    /// Set the Rules-Requires-Root field
613    pub fn set_rules_requires_root(&mut self, requires_root: bool) {
614        self.0.set(
615            "Rules-Requires-Root",
616            if requires_root { "yes" } else { "no" },
617        );
618    }
619
620    /// Return the Testsuite field
621    pub fn testsuite(&self) -> Option<String> {
622        self.0.get("Testsuite")
623    }
624
625    /// Set the Testsuite field
626    pub fn set_testsuite(&mut self, testsuite: &str) {
627        self.0.set("Testsuite", testsuite);
628    }
629
630    /// Check if this source paragraph's range overlaps with the given range
631    ///
632    /// # Arguments
633    /// * `range` - The text range to check for overlap
634    ///
635    /// # Returns
636    /// `true` if the paragraph overlaps with the given range, `false` otherwise
637    pub fn overlaps_range(&self, range: rowan::TextRange) -> bool {
638        let para_range = self.0.syntax().text_range();
639        para_range.start() < range.end() && range.start() < para_range.end()
640    }
641
642    /// Get fields in this source paragraph that overlap with the given range
643    ///
644    /// # Arguments
645    /// * `range` - The text range to check for overlaps
646    ///
647    /// # Returns
648    /// An iterator over Entry items that overlap with the given range
649    pub fn fields_in_range(
650        &self,
651        range: rowan::TextRange,
652    ) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
653        self.0.entries().filter(move |entry| {
654            let entry_range = entry.syntax().text_range();
655            entry_range.start() < range.end() && range.start() < entry_range.end()
656        })
657    }
658}
659
660#[cfg(feature = "python-debian")]
661impl<'py> pyo3::IntoPyObject<'py> for Source {
662    type Target = pyo3::PyAny;
663    type Output = pyo3::Bound<'py, Self::Target>;
664    type Error = pyo3::PyErr;
665
666    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
667        self.0.into_pyobject(py)
668    }
669}
670
671#[cfg(feature = "python-debian")]
672impl<'a, 'py> pyo3::IntoPyObject<'py> for &'a Source {
673    type Target = pyo3::PyAny;
674    type Output = pyo3::Bound<'py, Self::Target>;
675    type Error = pyo3::PyErr;
676
677    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
678        (&self.0).into_pyobject(py)
679    }
680}
681
682#[cfg(feature = "python-debian")]
683impl pyo3::FromPyObject<'_> for Source {
684    fn extract_bound(ob: &pyo3::Bound<pyo3::PyAny>) -> pyo3::PyResult<Self> {
685        use pyo3::prelude::*;
686        Ok(Source(ob.extract()?))
687    }
688}
689
690impl std::fmt::Display for Control {
691    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
692        self.0.fmt(f)
693    }
694}
695
696impl AstNode for Control {
697    type Language = deb822_lossless::Lang;
698
699    fn can_cast(kind: <Self::Language as rowan::Language>::Kind) -> bool {
700        Deb822::can_cast(kind)
701    }
702
703    fn cast(syntax: rowan::SyntaxNode<Self::Language>) -> Option<Self> {
704        Deb822::cast(syntax).map(Control)
705    }
706
707    fn syntax(&self) -> &rowan::SyntaxNode<Self::Language> {
708        self.0.syntax()
709    }
710}
711
712/// A binary package paragraph
713#[derive(Debug, Clone, PartialEq, Eq)]
714pub struct Binary(Paragraph);
715
716impl From<Binary> for Paragraph {
717    fn from(b: Binary) -> Self {
718        b.0
719    }
720}
721
722impl From<Paragraph> for Binary {
723    fn from(p: Paragraph) -> Self {
724        Binary(p)
725    }
726}
727
728#[cfg(feature = "python-debian")]
729impl<'py> pyo3::IntoPyObject<'py> for Binary {
730    type Target = pyo3::PyAny;
731    type Output = pyo3::Bound<'py, Self::Target>;
732    type Error = pyo3::PyErr;
733
734    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
735        self.0.into_pyobject(py)
736    }
737}
738
739#[cfg(feature = "python-debian")]
740impl<'a, 'py> pyo3::IntoPyObject<'py> for &'a Binary {
741    type Target = pyo3::PyAny;
742    type Output = pyo3::Bound<'py, Self::Target>;
743    type Error = pyo3::PyErr;
744
745    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
746        (&self.0).into_pyobject(py)
747    }
748}
749
750#[cfg(feature = "python-debian")]
751impl pyo3::FromPyObject<'_> for Binary {
752    fn extract_bound(ob: &pyo3::Bound<pyo3::PyAny>) -> pyo3::PyResult<Self> {
753        use pyo3::prelude::*;
754        Ok(Binary(ob.extract()?))
755    }
756}
757
758impl Default for Binary {
759    fn default() -> Self {
760        Self::new()
761    }
762}
763
764impl Binary {
765    /// Create a new binary package control file
766    pub fn new() -> Self {
767        Binary(Paragraph::new())
768    }
769
770    /// Return the underlying deb822 paragraph, mutable
771    pub fn as_mut_deb822(&mut self) -> &mut Paragraph {
772        &mut self.0
773    }
774
775    /// Return the underlying deb822 paragraph
776    pub fn as_deb822(&self) -> &Paragraph {
777        &self.0
778    }
779
780    /// Wrap and sort the control file
781    pub fn wrap_and_sort(
782        &mut self,
783        indentation: deb822_lossless::Indentation,
784        immediate_empty_line: bool,
785        max_line_length_one_liner: Option<usize>,
786    ) {
787        self.0 = self.0.wrap_and_sort(
788            indentation,
789            immediate_empty_line,
790            max_line_length_one_liner,
791            None,
792            Some(&format_field),
793        );
794    }
795
796    /// The name of the package.
797    pub fn name(&self) -> Option<String> {
798        self.0.get("Package")
799    }
800
801    /// Set the name of the package
802    pub fn set_name(&mut self, name: &str) {
803        self.0.set("Package", name);
804    }
805
806    /// The section of the package.
807    pub fn section(&self) -> Option<String> {
808        self.0.get("Section")
809    }
810
811    /// Set the section
812    pub fn set_section(&mut self, section: Option<&str>) {
813        if let Some(section) = section {
814            self.0.set("Section", section);
815        } else {
816            self.0.remove("Section");
817        }
818    }
819
820    /// The priority of the package.
821    pub fn priority(&self) -> Option<Priority> {
822        self.0.get("Priority").and_then(|v| v.parse().ok())
823    }
824
825    /// Set the priority of the package
826    pub fn set_priority(&mut self, priority: Option<Priority>) {
827        if let Some(priority) = priority {
828            self.0.set("Priority", priority.to_string().as_str());
829        } else {
830            self.0.remove("Priority");
831        }
832    }
833
834    /// The architecture of the package.
835    pub fn architecture(&self) -> Option<String> {
836        self.0.get("Architecture")
837    }
838
839    /// Set the architecture of the package
840    pub fn set_architecture(&mut self, arch: Option<&str>) {
841        if let Some(arch) = arch {
842            self.0.set("Architecture", arch);
843        } else {
844            self.0.remove("Architecture");
845        }
846    }
847
848    /// The dependencies of the package.
849    pub fn depends(&self) -> Option<Relations> {
850        self.0.get("Depends").map(|s| s.parse().unwrap())
851    }
852
853    /// Set the Depends field
854    pub fn set_depends(&mut self, depends: Option<&Relations>) {
855        if let Some(depends) = depends {
856            self.0.set("Depends", depends.to_string().as_str());
857        } else {
858            self.0.remove("Depends");
859        }
860    }
861
862    /// The package that this package recommends
863    pub fn recommends(&self) -> Option<Relations> {
864        self.0.get("Recommends").map(|s| s.parse().unwrap())
865    }
866
867    /// Set the Recommends field
868    pub fn set_recommends(&mut self, recommends: Option<&Relations>) {
869        if let Some(recommends) = recommends {
870            self.0.set("Recommends", recommends.to_string().as_str());
871        } else {
872            self.0.remove("Recommends");
873        }
874    }
875
876    /// Packages that this package suggests
877    pub fn suggests(&self) -> Option<Relations> {
878        self.0.get("Suggests").map(|s| s.parse().unwrap())
879    }
880
881    /// Set the Suggests field
882    pub fn set_suggests(&mut self, suggests: Option<&Relations>) {
883        if let Some(suggests) = suggests {
884            self.0.set("Suggests", suggests.to_string().as_str());
885        } else {
886            self.0.remove("Suggests");
887        }
888    }
889
890    /// The package that this package enhances
891    pub fn enhances(&self) -> Option<Relations> {
892        self.0.get("Enhances").map(|s| s.parse().unwrap())
893    }
894
895    /// Set the Enhances field
896    pub fn set_enhances(&mut self, enhances: Option<&Relations>) {
897        if let Some(enhances) = enhances {
898            self.0.set("Enhances", enhances.to_string().as_str());
899        } else {
900            self.0.remove("Enhances");
901        }
902    }
903
904    /// The package that this package pre-depends on
905    pub fn pre_depends(&self) -> Option<Relations> {
906        self.0.get("Pre-Depends").map(|s| s.parse().unwrap())
907    }
908
909    /// Set the Pre-Depends field
910    pub fn set_pre_depends(&mut self, pre_depends: Option<&Relations>) {
911        if let Some(pre_depends) = pre_depends {
912            self.0.set("Pre-Depends", pre_depends.to_string().as_str());
913        } else {
914            self.0.remove("Pre-Depends");
915        }
916    }
917
918    /// The package that this package breaks
919    pub fn breaks(&self) -> Option<Relations> {
920        self.0.get("Breaks").map(|s| s.parse().unwrap())
921    }
922
923    /// Set the Breaks field
924    pub fn set_breaks(&mut self, breaks: Option<&Relations>) {
925        if let Some(breaks) = breaks {
926            self.0.set("Breaks", breaks.to_string().as_str());
927        } else {
928            self.0.remove("Breaks");
929        }
930    }
931
932    /// The package that this package conflicts with
933    pub fn conflicts(&self) -> Option<Relations> {
934        self.0.get("Conflicts").map(|s| s.parse().unwrap())
935    }
936
937    /// Set the Conflicts field
938    pub fn set_conflicts(&mut self, conflicts: Option<&Relations>) {
939        if let Some(conflicts) = conflicts {
940            self.0.set("Conflicts", conflicts.to_string().as_str());
941        } else {
942            self.0.remove("Conflicts");
943        }
944    }
945
946    /// The package that this package replaces
947    pub fn replaces(&self) -> Option<Relations> {
948        self.0.get("Replaces").map(|s| s.parse().unwrap())
949    }
950
951    /// Set the Replaces field
952    pub fn set_replaces(&mut self, replaces: Option<&Relations>) {
953        if let Some(replaces) = replaces {
954            self.0.set("Replaces", replaces.to_string().as_str());
955        } else {
956            self.0.remove("Replaces");
957        }
958    }
959
960    /// Return the Provides field
961    pub fn provides(&self) -> Option<Relations> {
962        self.0.get("Provides").map(|s| s.parse().unwrap())
963    }
964
965    /// Set the Provides field
966    pub fn set_provides(&mut self, provides: Option<&Relations>) {
967        if let Some(provides) = provides {
968            self.0.set("Provides", provides.to_string().as_str());
969        } else {
970            self.0.remove("Provides");
971        }
972    }
973
974    /// Return the Built-Using field
975    pub fn built_using(&self) -> Option<Relations> {
976        self.0.get("Built-Using").map(|s| s.parse().unwrap())
977    }
978
979    /// Set the Built-Using field
980    pub fn set_built_using(&mut self, built_using: Option<&Relations>) {
981        if let Some(built_using) = built_using {
982            self.0.set("Built-Using", built_using.to_string().as_str());
983        } else {
984            self.0.remove("Built-Using");
985        }
986    }
987
988    /// The Multi-Arch field
989    pub fn multi_arch(&self) -> Option<MultiArch> {
990        self.0.get("Multi-Arch").map(|s| s.parse().unwrap())
991    }
992
993    /// Set the Multi-Arch field
994    pub fn set_multi_arch(&mut self, multi_arch: Option<MultiArch>) {
995        if let Some(multi_arch) = multi_arch {
996            self.0.set("Multi-Arch", multi_arch.to_string().as_str());
997        } else {
998            self.0.remove("Multi-Arch");
999        }
1000    }
1001
1002    /// Whether the package is essential
1003    pub fn essential(&self) -> bool {
1004        self.0.get("Essential").map(|s| s == "yes").unwrap_or(false)
1005    }
1006
1007    /// Set whether the package is essential
1008    pub fn set_essential(&mut self, essential: bool) {
1009        if essential {
1010            self.0.set("Essential", "yes");
1011        } else {
1012            self.0.remove("Essential");
1013        }
1014    }
1015
1016    /// Binary package description
1017    pub fn description(&self) -> Option<String> {
1018        self.0.get("Description")
1019    }
1020
1021    /// Set the binary package description
1022    pub fn set_description(&mut self, description: Option<&str>) {
1023        if let Some(description) = description {
1024            self.0.set("Description", description);
1025        } else {
1026            self.0.remove("Description");
1027        }
1028    }
1029
1030    /// Return the upstream homepage
1031    pub fn homepage(&self) -> Option<url::Url> {
1032        self.0.get("Homepage").and_then(|s| s.parse().ok())
1033    }
1034
1035    /// Set the upstream homepage
1036    pub fn set_homepage(&mut self, url: &url::Url) {
1037        self.0.set("Homepage", url.as_str());
1038    }
1039
1040    /// Check if this binary paragraph's range overlaps with the given range
1041    ///
1042    /// # Arguments
1043    /// * `range` - The text range to check for overlap
1044    ///
1045    /// # Returns
1046    /// `true` if the paragraph overlaps with the given range, `false` otherwise
1047    pub fn overlaps_range(&self, range: rowan::TextRange) -> bool {
1048        let para_range = self.0.syntax().text_range();
1049        para_range.start() < range.end() && range.start() < para_range.end()
1050    }
1051
1052    /// Get fields in this binary paragraph that overlap with the given range
1053    ///
1054    /// # Arguments
1055    /// * `range` - The text range to check for overlaps
1056    ///
1057    /// # Returns
1058    /// An iterator over Entry items that overlap with the given range
1059    pub fn fields_in_range(
1060        &self,
1061        range: rowan::TextRange,
1062    ) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
1063        self.0.entries().filter(move |entry| {
1064            let entry_range = entry.syntax().text_range();
1065            entry_range.start() < range.end() && range.start() < entry_range.end()
1066        })
1067    }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::*;
1073    use crate::relations::VersionConstraint;
1074    #[test]
1075    fn test_parse() {
1076        let control: Control = r#"Source: foo
1077Section: libs
1078Priority: optional
1079Build-Depends: bar (>= 1.0.0), baz (>= 1.0.0)
1080Homepage: https://example.com
1081
1082"#
1083        .parse()
1084        .unwrap();
1085        let source = control.source().unwrap();
1086
1087        assert_eq!(source.name(), Some("foo".to_owned()));
1088        assert_eq!(source.section(), Some("libs".to_owned()));
1089        assert_eq!(source.priority(), Some(super::Priority::Optional));
1090        assert_eq!(
1091            source.homepage(),
1092            Some("https://example.com".parse().unwrap())
1093        );
1094        let bd = source.build_depends().unwrap();
1095        let entries = bd.entries().collect::<Vec<_>>();
1096        assert_eq!(entries.len(), 2);
1097        let rel = entries[0].relations().collect::<Vec<_>>().pop().unwrap();
1098        assert_eq!(rel.name(), "bar");
1099        assert_eq!(
1100            rel.version(),
1101            Some((
1102                VersionConstraint::GreaterThanEqual,
1103                "1.0.0".parse().unwrap()
1104            ))
1105        );
1106        let rel = entries[1].relations().collect::<Vec<_>>().pop().unwrap();
1107        assert_eq!(rel.name(), "baz");
1108        assert_eq!(
1109            rel.version(),
1110            Some((
1111                VersionConstraint::GreaterThanEqual,
1112                "1.0.0".parse().unwrap()
1113            ))
1114        );
1115    }
1116
1117    #[test]
1118    fn test_description() {
1119        let control: Control = r#"Source: foo
1120
1121Package: foo
1122Description: this is the short description
1123 And the longer one
1124 .
1125 is on the next lines
1126"#
1127        .parse()
1128        .unwrap();
1129        let binary = control.binaries().next().unwrap();
1130        assert_eq!(
1131            binary.description(),
1132            Some(
1133                "this is the short description\nAnd the longer one\n.\nis on the next lines"
1134                    .to_owned()
1135            )
1136        );
1137    }
1138
1139    #[test]
1140    fn test_as_mut_deb822() {
1141        let mut control = Control::new();
1142        let deb822 = control.as_mut_deb822();
1143        let mut p = deb822.add_paragraph();
1144        p.set("Source", "foo");
1145        assert_eq!(control.source().unwrap().name(), Some("foo".to_owned()));
1146    }
1147
1148    #[test]
1149    fn test_as_deb822() {
1150        let control = Control::new();
1151        let _deb822: &Deb822 = control.as_deb822();
1152    }
1153
1154    #[test]
1155    fn test_set_depends() {
1156        let mut control = Control::new();
1157        let mut binary = control.add_binary("foo");
1158        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1159        binary.set_depends(Some(&relations));
1160    }
1161
1162    #[test]
1163    fn test_wrap_and_sort() {
1164        let mut control: Control = r#"Package: blah
1165Section:     libs
1166
1167
1168
1169Package: foo
1170Description: this is a 
1171      bar
1172      blah
1173"#
1174        .parse()
1175        .unwrap();
1176        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), false, None);
1177        let expected = r#"Package: blah
1178Section: libs
1179
1180Package: foo
1181Description: this is a 
1182  bar
1183  blah
1184"#
1185        .to_owned();
1186        assert_eq!(control.to_string(), expected);
1187    }
1188
1189    #[test]
1190    fn test_wrap_and_sort_source() {
1191        let mut control: Control = r#"Source: blah
1192Depends: foo, bar   (<=  1.0.0)
1193
1194"#
1195        .parse()
1196        .unwrap();
1197        control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), true, None);
1198        let expected = r#"Source: blah
1199Depends: bar (<= 1.0.0), foo
1200"#
1201        .to_owned();
1202        assert_eq!(control.to_string(), expected);
1203    }
1204
1205    #[test]
1206    fn test_source_wrap_and_sort() {
1207        let control: Control = r#"Source: blah
1208Build-Depends: foo, bar (>= 1.0.0)
1209
1210"#
1211        .parse()
1212        .unwrap();
1213        let mut source = control.source().unwrap();
1214        source.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), true, None);
1215        // The actual behavior - the method modifies the source in-place
1216        // but doesn't automatically affect the overall control structure
1217        // So we just test that the method executes without error
1218        assert!(source.build_depends().is_some());
1219    }
1220
1221    #[test]
1222    fn test_binary_set_breaks() {
1223        let mut control = Control::new();
1224        let mut binary = control.add_binary("foo");
1225        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1226        binary.set_breaks(Some(&relations));
1227        assert!(binary.breaks().is_some());
1228    }
1229
1230    #[test]
1231    fn test_binary_set_pre_depends() {
1232        let mut control = Control::new();
1233        let mut binary = control.add_binary("foo");
1234        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1235        binary.set_pre_depends(Some(&relations));
1236        assert!(binary.pre_depends().is_some());
1237    }
1238
1239    #[test]
1240    fn test_binary_set_provides() {
1241        let mut control = Control::new();
1242        let mut binary = control.add_binary("foo");
1243        let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
1244        binary.set_provides(Some(&relations));
1245        assert!(binary.provides().is_some());
1246    }
1247
1248    #[test]
1249    fn test_source_build_conflicts() {
1250        let control: Control = r#"Source: blah
1251Build-Conflicts: foo, bar (>= 1.0.0)
1252
1253"#
1254        .parse()
1255        .unwrap();
1256        let source = control.source().unwrap();
1257        let conflicts = source.build_conflicts();
1258        assert!(conflicts.is_some());
1259    }
1260
1261    #[test]
1262    fn test_source_vcs_svn() {
1263        let control: Control = r#"Source: blah
1264Vcs-Svn: https://example.com/svn/repo
1265
1266"#
1267        .parse()
1268        .unwrap();
1269        let source = control.source().unwrap();
1270        assert_eq!(
1271            source.vcs_svn(),
1272            Some("https://example.com/svn/repo".to_string())
1273        );
1274    }
1275
1276    #[test]
1277    fn test_control_from_conversion() {
1278        let deb822_data = r#"Source: test
1279Section: libs
1280
1281"#;
1282        let deb822: Deb822 = deb822_data.parse().unwrap();
1283        let control = Control::from(deb822);
1284        assert!(control.source().is_some());
1285    }
1286
1287    #[test]
1288    fn test_fields_in_range() {
1289        let control_text = r#"Source: test-package
1290Maintainer: Test User <test@example.com>
1291Build-Depends: debhelper (>= 12)
1292
1293Package: test-binary
1294Architecture: any
1295Depends: ${shlibs:Depends}
1296Description: Test package
1297 This is a test package
1298"#;
1299        let control: Control = control_text.parse().unwrap();
1300
1301        // Test range that covers only the Source field
1302        let source_start = 0;
1303        let source_end = "Source: test-package".len();
1304        let source_range =
1305            rowan::TextRange::new((source_start as u32).into(), (source_end as u32).into());
1306
1307        let fields: Vec<_> = control.fields_in_range(source_range).collect();
1308        assert_eq!(fields.len(), 1);
1309        assert_eq!(fields[0].key(), Some("Source".to_string()));
1310
1311        // Test range that covers multiple fields in source paragraph
1312        let maintainer_start = control_text.find("Maintainer:").unwrap();
1313        let build_depends_end = control_text
1314            .find("Build-Depends: debhelper (>= 12)")
1315            .unwrap()
1316            + "Build-Depends: debhelper (>= 12)".len();
1317        let multi_range = rowan::TextRange::new(
1318            (maintainer_start as u32).into(),
1319            (build_depends_end as u32).into(),
1320        );
1321
1322        let fields: Vec<_> = control.fields_in_range(multi_range).collect();
1323        assert_eq!(fields.len(), 2);
1324        assert_eq!(fields[0].key(), Some("Maintainer".to_string()));
1325        assert_eq!(fields[1].key(), Some("Build-Depends".to_string()));
1326
1327        // Test range that spans across paragraphs
1328        let cross_para_start = control_text.find("Build-Depends:").unwrap();
1329        let cross_para_end =
1330            control_text.find("Architecture: any").unwrap() + "Architecture: any".len();
1331        let cross_range = rowan::TextRange::new(
1332            (cross_para_start as u32).into(),
1333            (cross_para_end as u32).into(),
1334        );
1335
1336        let fields: Vec<_> = control.fields_in_range(cross_range).collect();
1337        assert_eq!(fields.len(), 3); // Build-Depends, Package, Architecture
1338        assert_eq!(fields[0].key(), Some("Build-Depends".to_string()));
1339        assert_eq!(fields[1].key(), Some("Package".to_string()));
1340        assert_eq!(fields[2].key(), Some("Architecture".to_string()));
1341
1342        // Test empty range (should return no fields)
1343        let empty_range = rowan::TextRange::new(1000.into(), 1001.into());
1344        let fields: Vec<_> = control.fields_in_range(empty_range).collect();
1345        assert_eq!(fields.len(), 0);
1346    }
1347
1348    #[test]
1349    fn test_source_overlaps_range() {
1350        let control_text = r#"Source: test-package
1351Maintainer: Test User <test@example.com>
1352
1353Package: test-binary
1354Architecture: any
1355"#;
1356        let control: Control = control_text.parse().unwrap();
1357        let source = control.source().unwrap();
1358
1359        // Test range that overlaps with source paragraph
1360        let overlap_range = rowan::TextRange::new(10.into(), 30.into());
1361        assert!(source.overlaps_range(overlap_range));
1362
1363        // Test range that doesn't overlap with source paragraph
1364        let binary_start = control_text.find("Package:").unwrap();
1365        let no_overlap_range = rowan::TextRange::new(
1366            (binary_start as u32).into(),
1367            ((binary_start + 20) as u32).into(),
1368        );
1369        assert!(!source.overlaps_range(no_overlap_range));
1370
1371        // Test range that starts before and ends within source paragraph
1372        let partial_overlap = rowan::TextRange::new(0.into(), 15.into());
1373        assert!(source.overlaps_range(partial_overlap));
1374    }
1375
1376    #[test]
1377    fn test_source_fields_in_range() {
1378        let control_text = r#"Source: test-package
1379Maintainer: Test User <test@example.com>
1380Build-Depends: debhelper (>= 12)
1381
1382Package: test-binary
1383"#;
1384        let control: Control = control_text.parse().unwrap();
1385        let source = control.source().unwrap();
1386
1387        // Test range covering Maintainer field
1388        let maintainer_start = control_text.find("Maintainer:").unwrap();
1389        let maintainer_end = maintainer_start + "Maintainer: Test User <test@example.com>".len();
1390        let maintainer_range = rowan::TextRange::new(
1391            (maintainer_start as u32).into(),
1392            (maintainer_end as u32).into(),
1393        );
1394
1395        let fields: Vec<_> = source.fields_in_range(maintainer_range).collect();
1396        assert_eq!(fields.len(), 1);
1397        assert_eq!(fields[0].key(), Some("Maintainer".to_string()));
1398
1399        // Test range covering multiple fields
1400        let all_source_range = rowan::TextRange::new(0.into(), 100.into());
1401        let fields: Vec<_> = source.fields_in_range(all_source_range).collect();
1402        assert_eq!(fields.len(), 3); // Source, Maintainer, Build-Depends
1403    }
1404
1405    #[test]
1406    fn test_binary_overlaps_range() {
1407        let control_text = r#"Source: test-package
1408
1409Package: test-binary
1410Architecture: any
1411Depends: ${shlibs:Depends}
1412"#;
1413        let control: Control = control_text.parse().unwrap();
1414        let binary = control.binaries().next().unwrap();
1415
1416        // Test range that overlaps with binary paragraph
1417        let package_start = control_text.find("Package:").unwrap();
1418        let overlap_range = rowan::TextRange::new(
1419            (package_start as u32).into(),
1420            ((package_start + 30) as u32).into(),
1421        );
1422        assert!(binary.overlaps_range(overlap_range));
1423
1424        // Test range before binary paragraph
1425        let no_overlap_range = rowan::TextRange::new(0.into(), 10.into());
1426        assert!(!binary.overlaps_range(no_overlap_range));
1427    }
1428
1429    #[test]
1430    fn test_binary_fields_in_range() {
1431        let control_text = r#"Source: test-package
1432
1433Package: test-binary
1434Architecture: any
1435Depends: ${shlibs:Depends}
1436Description: Test binary
1437 This is a test binary package
1438"#;
1439        let control: Control = control_text.parse().unwrap();
1440        let binary = control.binaries().next().unwrap();
1441
1442        // Test range covering Architecture and Depends
1443        let arch_start = control_text.find("Architecture:").unwrap();
1444        let depends_end = control_text.find("Depends: ${shlibs:Depends}").unwrap()
1445            + "Depends: ${shlibs:Depends}".len();
1446        let range = rowan::TextRange::new((arch_start as u32).into(), (depends_end as u32).into());
1447
1448        let fields: Vec<_> = binary.fields_in_range(range).collect();
1449        assert_eq!(fields.len(), 2);
1450        assert_eq!(fields[0].key(), Some("Architecture".to_string()));
1451        assert_eq!(fields[1].key(), Some("Depends".to_string()));
1452
1453        // Test partial overlap with Description field
1454        let desc_start = control_text.find("Description:").unwrap();
1455        let partial_range = rowan::TextRange::new(
1456            ((desc_start + 5) as u32).into(),
1457            ((desc_start + 15) as u32).into(),
1458        );
1459        let fields: Vec<_> = binary.fields_in_range(partial_range).collect();
1460        assert_eq!(fields.len(), 1);
1461        assert_eq!(fields[0].key(), Some("Description".to_string()));
1462    }
1463
1464    #[test]
1465    fn test_incremental_parsing_use_case() {
1466        // This test simulates a real LSP use case where only changed fields are processed
1467        let control_text = r#"Source: example
1468Maintainer: John Doe <john@example.com>
1469Standards-Version: 4.6.0
1470Build-Depends: debhelper-compat (= 13)
1471
1472Package: example-bin
1473Architecture: all
1474Depends: ${misc:Depends}
1475Description: Example package
1476 This is an example.
1477"#;
1478        let control: Control = control_text.parse().unwrap();
1479
1480        // Simulate a change to Standards-Version field
1481        let change_start = control_text.find("Standards-Version:").unwrap();
1482        let change_end = change_start + "Standards-Version: 4.6.0".len();
1483        let change_range =
1484            rowan::TextRange::new((change_start as u32).into(), (change_end as u32).into());
1485
1486        // Only process fields in the changed range
1487        let affected_fields: Vec<_> = control.fields_in_range(change_range).collect();
1488        assert_eq!(affected_fields.len(), 1);
1489        assert_eq!(
1490            affected_fields[0].key(),
1491            Some("Standards-Version".to_string())
1492        );
1493
1494        // Verify that we're not processing unrelated fields
1495        for entry in &affected_fields {
1496            let key = entry.key().unwrap();
1497            assert_ne!(key, "Maintainer");
1498            assert_ne!(key, "Build-Depends");
1499            assert_ne!(key, "Architecture");
1500        }
1501    }
1502
1503    #[test]
1504    fn test_positioned_parse_errors() {
1505        // Test case from the requirements document
1506        let input = "Invalid: field\nBroken field without colon";
1507        let parsed = Control::parse(input);
1508
1509        // Should have positioned errors accessible
1510        let positioned_errors = parsed.positioned_errors();
1511        assert!(
1512            !positioned_errors.is_empty(),
1513            "Should have positioned errors"
1514        );
1515
1516        // Test that we can access error properties
1517        for error in positioned_errors {
1518            let start_offset: u32 = error.range.start().into();
1519            let end_offset: u32 = error.range.end().into();
1520
1521            // Verify we have meaningful error messages
1522            assert!(!error.message.is_empty());
1523
1524            // Verify ranges are valid
1525            assert!(start_offset <= end_offset);
1526            assert!(end_offset <= input.len() as u32);
1527
1528            // Error should have a code
1529            assert!(error.code.is_some());
1530
1531            println!(
1532                "Error at {:?}: {} (code: {:?})",
1533                error.range, error.message, error.code
1534            );
1535        }
1536
1537        // Should also be able to get string errors for backward compatibility
1538        let string_errors = parsed.errors();
1539        assert!(!string_errors.is_empty());
1540        assert_eq!(string_errors.len(), positioned_errors.len());
1541    }
1542}