debian_control/lossless/
relations.rs

1//! Parser for relationship fields like `Depends`, `Recommends`, etc.
2//!
3//! # Example
4//! ```
5//! use debian_control::lossless::relations::{Relations, Relation};
6//! use debian_control::relations::VersionConstraint;
7//!
8//! let mut relations: Relations = r"python3-dulwich (>= 0.19.0), python3-requests, python3-urllib3 (<< 1.26.0)".parse().unwrap();
9//! assert_eq!(relations.to_string(), "python3-dulwich (>= 0.19.0), python3-requests, python3-urllib3 (<< 1.26.0)");
10//! assert!(relations.satisfied_by(|name: &str| -> Option<debversion::Version> {
11//!    match name {
12//!    "python3-dulwich" => Some("0.19.0".parse().unwrap()),
13//!    "python3-requests" => Some("2.25.1".parse().unwrap()),
14//!    "python3-urllib3" => Some("1.25.11".parse().unwrap()),
15//!    _ => None
16//!    }}));
17//! relations.remove_entry(1);
18//! relations.get_entry(0).unwrap().get_relation(0).unwrap().set_archqual("amd64");
19//! assert_eq!(relations.to_string(), "python3-dulwich:amd64 (>= 0.19.0), python3-urllib3 (<< 1.26.0)");
20//! ```
21use crate::relations::SyntaxKind::{self, *};
22use crate::relations::{BuildProfile, VersionConstraint};
23use debversion::Version;
24use rowan::{Direction, NodeOrToken};
25use std::collections::HashSet;
26
27/// Error type for parsing relations fields
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct ParseError(Vec<String>);
30
31impl std::fmt::Display for ParseError {
32    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
33        for err in &self.0 {
34            writeln!(f, "{}", err)?;
35        }
36        Ok(())
37    }
38}
39
40impl std::error::Error for ParseError {}
41
42/// Second, implementing the `Language` trait teaches rowan to convert between
43/// these two SyntaxKind types, allowing for a nicer SyntaxNode API where
44/// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46enum Lang {}
47impl rowan::Language for Lang {
48    type Kind = SyntaxKind;
49    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
50        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
51    }
52    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
53        kind.into()
54    }
55}
56
57/// GreenNode is an immutable tree, which is cheap to change,
58/// but doesn't contain offsets and parent pointers.
59use rowan::{GreenNode, GreenToken};
60
61/// You can construct GreenNodes by hand, but a builder
62/// is helpful for top-down parsers: it maintains a stack
63/// of currently in-progress nodes
64use rowan::GreenNodeBuilder;
65
66/// The parse results are stored as a "green tree".
67/// We'll discuss working with the results later
68struct Parse {
69    green_node: GreenNode,
70    #[allow(unused)]
71    errors: Vec<String>,
72}
73
74fn parse(text: &str, allow_substvar: bool) -> Parse {
75    struct Parser {
76        /// input tokens, including whitespace,
77        /// in *reverse* order.
78        tokens: Vec<(SyntaxKind, String)>,
79        /// the in-progress tree.
80        builder: GreenNodeBuilder<'static>,
81        /// the list of syntax errors we've accumulated
82        /// so far.
83        errors: Vec<String>,
84        /// whether to allow substvars
85        allow_substvar: bool,
86    }
87
88    impl Parser {
89        fn parse_substvar(&mut self) {
90            self.builder.start_node(SyntaxKind::SUBSTVAR.into());
91            self.bump();
92            if self.current() != Some(L_CURLY) {
93                self.error(format!("expected {{ but got {:?}", self.current()).to_string());
94            } else {
95                self.bump();
96            }
97            loop {
98                match self.current() {
99                    Some(IDENT) | Some(COLON) => {
100                        self.bump();
101                    }
102                    Some(R_CURLY) => {
103                        break;
104                    }
105                    e => {
106                        self.error(format!("expected identifier or : but got {:?}", e).to_string());
107                    }
108                }
109            }
110            if self.current() != Some(R_CURLY) {
111                self.error(format!("expected }} but got {:?}", self.current()).to_string());
112            } else {
113                self.bump();
114            }
115            self.builder.finish_node();
116        }
117
118        fn parse_entry(&mut self) {
119            self.skip_ws();
120            self.builder.start_node(SyntaxKind::ENTRY.into());
121            loop {
122                self.parse_relation();
123                match self.peek_past_ws() {
124                    Some(COMMA) => {
125                        break;
126                    }
127                    Some(PIPE) => {
128                        self.skip_ws();
129                        self.bump();
130                        self.skip_ws();
131                    }
132                    None => {
133                        self.skip_ws();
134                        break;
135                    }
136                    _ => {
137                        self.skip_ws();
138                        self.builder.start_node(SyntaxKind::ERROR.into());
139                        match self.tokens.pop() {
140                            Some((k, t)) => {
141                                self.builder.token(k.into(), t.as_str());
142                                self.errors
143                                    .push(format!("Expected comma or pipe, not {:?}", (k, t)));
144                            }
145                            None => {
146                                self.errors
147                                    .push("Expected comma or pipe, got end of file".to_string());
148                            }
149                        }
150                        self.builder.finish_node();
151                    }
152                }
153            }
154            self.builder.finish_node();
155        }
156
157        fn error(&mut self, error: String) {
158            self.errors.push(error);
159            self.builder.start_node(SyntaxKind::ERROR.into());
160            if self.current().is_some() {
161                self.bump();
162            }
163            self.builder.finish_node();
164        }
165
166        fn parse_relation(&mut self) {
167            self.builder.start_node(SyntaxKind::RELATION.into());
168            if self.current() == Some(IDENT) {
169                self.bump();
170            } else {
171                self.error("Expected package name".to_string());
172            }
173            match self.peek_past_ws() {
174                Some(COLON) => {
175                    self.skip_ws();
176                    self.builder.start_node(ARCHQUAL.into());
177                    self.bump();
178                    self.skip_ws();
179                    if self.current() == Some(IDENT) {
180                        self.bump();
181                    } else {
182                        self.error("Expected architecture name".to_string());
183                    }
184                    self.builder.finish_node();
185                    self.skip_ws();
186                }
187                Some(PIPE) | Some(COMMA) => {}
188                None | Some(L_PARENS) | Some(L_BRACKET) | Some(L_ANGLE) => {
189                    self.skip_ws();
190                }
191                e => {
192                    self.skip_ws();
193                    self.error(format!(
194                        "Expected ':' or '|' or '[' or '<' or ',' but got {:?}",
195                        e
196                    ));
197                }
198            }
199
200            if self.peek_past_ws() == Some(L_PARENS) {
201                self.skip_ws();
202                self.builder.start_node(VERSION.into());
203                self.bump();
204                self.skip_ws();
205
206                self.builder.start_node(CONSTRAINT.into());
207
208                while self.current() == Some(L_ANGLE)
209                    || self.current() == Some(R_ANGLE)
210                    || self.current() == Some(EQUAL)
211                {
212                    self.bump();
213                }
214
215                self.builder.finish_node();
216
217                self.skip_ws();
218
219                // Read IDENT and COLON tokens until we see R_PARENS
220                // This handles version strings with epochs (e.g., "1:2.3.2-2~")
221                while matches!(self.current(), Some(IDENT) | Some(COLON)) {
222                    self.bump();
223                }
224
225                if self.current() == Some(R_PARENS) {
226                    self.bump();
227                } else {
228                    self.error("Expected ')'".to_string());
229                }
230
231                self.builder.finish_node();
232            }
233
234            if self.peek_past_ws() == Some(L_BRACKET) {
235                self.skip_ws();
236                self.builder.start_node(ARCHITECTURES.into());
237                self.bump();
238                loop {
239                    self.skip_ws();
240                    match self.current() {
241                        Some(NOT) => {
242                            self.bump();
243                        }
244                        Some(IDENT) => {
245                            self.bump();
246                        }
247                        Some(R_BRACKET) => {
248                            self.bump();
249                            break;
250                        }
251                        _ => {
252                            self.error("Expected architecture name or '!' or ']'".to_string());
253                        }
254                    }
255                }
256                self.builder.finish_node();
257            }
258
259            while self.peek_past_ws() == Some(L_ANGLE) {
260                self.skip_ws();
261                self.builder.start_node(PROFILES.into());
262                self.bump();
263
264                loop {
265                    self.skip_ws();
266                    match self.current() {
267                        Some(IDENT) => {
268                            self.bump();
269                        }
270                        Some(NOT) => {
271                            self.bump();
272                            self.skip_ws();
273                            if self.current() == Some(IDENT) {
274                                self.bump();
275                            } else {
276                                self.error("Expected profile".to_string());
277                            }
278                        }
279                        Some(R_ANGLE) => {
280                            self.bump();
281                            break;
282                        }
283                        None => {
284                            self.error("Expected profile or '>'".to_string());
285                            break;
286                        }
287                        _ => {
288                            self.error("Expected profile or '!' or '>'".to_string());
289                        }
290                    }
291                }
292
293                self.builder.finish_node();
294            }
295
296            self.builder.finish_node();
297        }
298
299        fn parse(mut self) -> Parse {
300            self.builder.start_node(SyntaxKind::ROOT.into());
301
302            self.skip_ws();
303
304            while self.current().is_some() {
305                match self.current() {
306                    Some(IDENT) => self.parse_entry(),
307                    Some(DOLLAR) => {
308                        if self.allow_substvar {
309                            self.parse_substvar()
310                        } else {
311                            self.error("Substvars are not allowed".to_string());
312                        }
313                    }
314                    Some(COMMA) => {
315                        // Empty entry, but that's okay - probably?
316                    }
317                    Some(c) => {
318                        self.error(format!("expected $ or identifier but got {:?}", c));
319                    }
320                    None => {
321                        self.error("expected identifier but got end of file".to_string());
322                    }
323                }
324
325                self.skip_ws();
326                match self.current() {
327                    Some(COMMA) => {
328                        self.bump();
329                    }
330                    None => {
331                        break;
332                    }
333                    c => {
334                        self.error(format!("expected comma or end of file but got {:?}", c));
335                    }
336                }
337                self.skip_ws();
338            }
339
340            self.builder.finish_node();
341            // Turn the builder into a GreenNode
342            Parse {
343                green_node: self.builder.finish(),
344                errors: self.errors,
345            }
346        }
347        /// Advance one token, adding it to the current branch of the tree builder.
348        fn bump(&mut self) {
349            let (kind, text) = self.tokens.pop().unwrap();
350            self.builder.token(kind.into(), text.as_str());
351        }
352        /// Peek at the first unprocessed token
353        fn current(&self) -> Option<SyntaxKind> {
354            self.tokens.last().map(|(kind, _)| *kind)
355        }
356        fn skip_ws(&mut self) {
357            while self.current() == Some(WHITESPACE) || self.current() == Some(NEWLINE) {
358                self.bump()
359            }
360        }
361
362        fn peek_past_ws(&self) -> Option<SyntaxKind> {
363            let mut i = self.tokens.len();
364            while i > 0 {
365                i -= 1;
366                match self.tokens[i].0 {
367                    WHITESPACE | NEWLINE => {}
368                    _ => return Some(self.tokens[i].0),
369                }
370            }
371            None
372        }
373    }
374
375    let mut tokens = crate::relations::lex(text);
376    tokens.reverse();
377    Parser {
378        tokens,
379        builder: GreenNodeBuilder::new(),
380        errors: Vec::new(),
381        allow_substvar,
382    }
383    .parse()
384}
385
386// To work with the parse results we need a view into the
387// green tree - the Syntax tree.
388// It is also immutable, like a GreenNode,
389// but it contains parent pointers, offsets, and
390// has identity semantics.
391
392type SyntaxNode = rowan::SyntaxNode<Lang>;
393#[allow(unused)]
394type SyntaxToken = rowan::SyntaxToken<Lang>;
395#[allow(unused)]
396type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
397
398impl Parse {
399    fn root_mut(&self) -> Relations {
400        Relations::cast(SyntaxNode::new_root_mut(self.green_node.clone())).unwrap()
401    }
402}
403
404macro_rules! ast_node {
405    ($ast:ident, $kind:ident) => {
406        /// A node in the syntax tree representing a $ast
407        #[repr(transparent)]
408        pub struct $ast(SyntaxNode);
409        impl $ast {
410            #[allow(unused)]
411            fn cast(node: SyntaxNode) -> Option<Self> {
412                if node.kind() == $kind {
413                    Some(Self(node))
414                } else {
415                    None
416                }
417            }
418        }
419
420        impl std::fmt::Display for $ast {
421            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422                f.write_str(&self.0.text().to_string())
423            }
424        }
425    };
426}
427
428ast_node!(Relations, ROOT);
429ast_node!(Entry, ENTRY);
430ast_node!(Relation, RELATION);
431ast_node!(Substvar, SUBSTVAR);
432
433impl PartialEq for Relations {
434    fn eq(&self, other: &Self) -> bool {
435        self.entries().collect::<Vec<_>>() == other.entries().collect::<Vec<_>>()
436    }
437}
438
439impl PartialEq for Entry {
440    fn eq(&self, other: &Self) -> bool {
441        self.relations().collect::<Vec<_>>() == other.relations().collect::<Vec<_>>()
442    }
443}
444
445impl PartialEq for Relation {
446    fn eq(&self, other: &Self) -> bool {
447        self.name() == other.name()
448            && self.version() == other.version()
449            && self.archqual() == other.archqual()
450            && self.architectures().map(|x| x.collect::<HashSet<_>>())
451                == other.architectures().map(|x| x.collect::<HashSet<_>>())
452            && self.profiles().eq(other.profiles())
453    }
454}
455
456#[cfg(feature = "serde")]
457impl serde::Serialize for Relations {
458    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
459        let rep = self.to_string();
460        serializer.serialize_str(&rep)
461    }
462}
463
464#[cfg(feature = "serde")]
465impl<'de> serde::Deserialize<'de> for Relations {
466    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
467        let s = String::deserialize(deserializer)?;
468        let relations = s.parse().map_err(serde::de::Error::custom)?;
469        Ok(relations)
470    }
471}
472
473impl std::fmt::Debug for Relations {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        let mut s = f.debug_struct("Relations");
476
477        for entry in self.entries() {
478            s.field("entry", &entry);
479        }
480
481        s.finish()
482    }
483}
484
485impl std::fmt::Debug for Entry {
486    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
487        let mut s = f.debug_struct("Entry");
488
489        for relation in self.relations() {
490            s.field("relation", &relation);
491        }
492
493        s.finish()
494    }
495}
496
497#[cfg(feature = "serde")]
498impl serde::Serialize for Entry {
499    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
500        let rep = self.to_string();
501        serializer.serialize_str(&rep)
502    }
503}
504
505#[cfg(feature = "serde")]
506impl<'de> serde::Deserialize<'de> for Entry {
507    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
508        let s = String::deserialize(deserializer)?;
509        let entry = s.parse().map_err(serde::de::Error::custom)?;
510        Ok(entry)
511    }
512}
513
514impl std::fmt::Debug for Relation {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        let mut s = f.debug_struct("Relation");
517
518        s.field("name", &self.name());
519
520        if let Some((vc, version)) = self.version() {
521            s.field("version", &vc);
522            s.field("version", &version);
523        }
524
525        s.finish()
526    }
527}
528
529#[cfg(feature = "serde")]
530impl serde::Serialize for Relation {
531    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
532        let rep = self.to_string();
533        serializer.serialize_str(&rep)
534    }
535}
536
537#[cfg(feature = "serde")]
538impl<'de> serde::Deserialize<'de> for Relation {
539    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
540        let s = String::deserialize(deserializer)?;
541        let relation = s.parse().map_err(serde::de::Error::custom)?;
542        Ok(relation)
543    }
544}
545
546impl Default for Relations {
547    fn default() -> Self {
548        Self::new()
549    }
550}
551
552/// Check if a package name should be treated as special for sorting purposes.
553///
554/// Special names include substitution variables (like `${misc:Depends}`)
555/// and template variables (like `@cdbs@`).
556fn is_special_package_name(name: &str) -> bool {
557    // Substitution variables like ${misc:Depends}
558    if name.starts_with("${") && name.ends_with('}') {
559        return true;
560    }
561    // Template variables like @cdbs@
562    if name.starts_with('@') && name.ends_with('@') {
563        return true;
564    }
565    false
566}
567
568/// Trait for defining sorting order of package relations.
569pub trait SortingOrder {
570    /// Compare two package names for sorting.
571    ///
572    /// Returns true if `name1` should come before `name2`.
573    fn lt(&self, name1: &str, name2: &str) -> bool;
574
575    /// Check if a package name should be ignored for sorting purposes.
576    fn ignore(&self, name: &str) -> bool;
577}
578
579/// Default sorting order (lexicographical with special items last).
580#[derive(Debug, Clone, Copy, Default)]
581pub struct DefaultSortingOrder;
582
583impl SortingOrder for DefaultSortingOrder {
584    fn lt(&self, name1: &str, name2: &str) -> bool {
585        let special1 = is_special_package_name(name1);
586        let special2 = is_special_package_name(name2);
587
588        // Special items always come last
589        if special1 && !special2 {
590            return false;
591        }
592        if !special1 && special2 {
593            return true;
594        }
595        if special1 && special2 {
596            // Both special - maintain original order
597            return false;
598        }
599
600        // Both are regular packages, use alphabetical order
601        name1 < name2
602    }
603
604    fn ignore(&self, name: &str) -> bool {
605        is_special_package_name(name)
606    }
607}
608
609/// Sorting order matching wrap-and-sort behavior.
610///
611/// This sorting order matches the behavior of the devscripts wrap-and-sort tool.
612/// It sorts packages into three groups:
613/// 1. Build-system packages (debhelper-compat, cdbs, etc.) - sorted first
614/// 2. Regular packages starting with [a-z0-9] - sorted in the middle
615/// 3. Substvars and other special packages - sorted last
616///
617/// Within each group, packages are sorted lexicographically.
618#[derive(Debug, Clone, Copy, Default)]
619pub struct WrapAndSortOrder;
620
621impl WrapAndSortOrder {
622    /// Build systems that should be sorted first, matching wrap-and-sort
623    const BUILD_SYSTEMS: &'static [&'static str] = &[
624        "cdbs",
625        "debhelper-compat",
626        "debhelper",
627        "debputy",
628        "dpkg-build-api",
629        "dpkg-dev",
630    ];
631
632    fn get_sort_key<'a>(&self, name: &'a str) -> (i32, &'a str) {
633        // Check if it's a build system (including dh-* packages)
634        if Self::BUILD_SYSTEMS.contains(&name) || name.starts_with("dh-") {
635            return (-1, name);
636        }
637
638        // Check if it starts with a regular character
639        if name
640            .chars()
641            .next()
642            .is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
643        {
644            return (0, name);
645        }
646
647        // Special packages (substvars, etc.) go last
648        (1, name)
649    }
650}
651
652impl SortingOrder for WrapAndSortOrder {
653    fn lt(&self, name1: &str, name2: &str) -> bool {
654        self.get_sort_key(name1) < self.get_sort_key(name2)
655    }
656
657    fn ignore(&self, _name: &str) -> bool {
658        // wrap-and-sort doesn't ignore any packages - it sorts everything
659        false
660    }
661}
662
663impl Relations {
664    /// Create a new relations field
665    pub fn new() -> Self {
666        Self::from(vec![])
667    }
668
669    /// Wrap and sort this relations field
670    #[must_use]
671    pub fn wrap_and_sort(self) -> Self {
672        let mut entries = self
673            .entries()
674            .map(|e| e.wrap_and_sort())
675            .collect::<Vec<_>>();
676        entries.sort();
677        // TODO: preserve comments
678        Self::from(entries)
679    }
680
681    /// Iterate over the entries in this relations field
682    pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
683        self.0.children().filter_map(Entry::cast)
684    }
685
686    /// Iterate over the entries in this relations field
687    pub fn iter(&self) -> impl Iterator<Item = Entry> + '_ {
688        self.entries()
689    }
690
691    /// Remove the entry at the given index
692    pub fn get_entry(&self, idx: usize) -> Option<Entry> {
693        self.entries().nth(idx)
694    }
695
696    /// Remove the entry at the given index
697    pub fn remove_entry(&mut self, idx: usize) -> Entry {
698        let mut entry = self.get_entry(idx).unwrap();
699        entry.remove();
700        entry
701    }
702
703    /// Helper to collect all consecutive WHITESPACE/NEWLINE tokens starting from a node
704    fn collect_whitespace(start: Option<NodeOrToken<SyntaxNode, SyntaxToken>>) -> String {
705        let mut pattern = String::new();
706        let mut current = start;
707        while let Some(token) = current {
708            if matches!(token.kind(), WHITESPACE | NEWLINE) {
709                if let NodeOrToken::Token(t) = &token {
710                    pattern.push_str(t.text());
711                }
712                current = token.next_sibling_or_token();
713            } else {
714                break;
715            }
716        }
717        pattern
718    }
719
720    /// Helper to convert a NodeOrToken to its green equivalent
721    fn to_green(node: &NodeOrToken<SyntaxNode, SyntaxToken>) -> NodeOrToken<GreenNode, GreenToken> {
722        match node {
723            NodeOrToken::Node(n) => NodeOrToken::Node(n.green().into()),
724            NodeOrToken::Token(t) => NodeOrToken::Token(t.green().to_owned()),
725        }
726    }
727
728    /// Helper to check if a token is whitespace
729    fn is_whitespace_token(token: &GreenToken) -> bool {
730        token.kind() == rowan::SyntaxKind(WHITESPACE as u16)
731            || token.kind() == rowan::SyntaxKind(NEWLINE as u16)
732    }
733
734    /// Helper to strip trailing whitespace tokens from a list of green children
735    fn strip_trailing_ws_from_children(
736        mut children: Vec<NodeOrToken<GreenNode, GreenToken>>,
737    ) -> Vec<NodeOrToken<GreenNode, GreenToken>> {
738        while let Some(last) = children.last() {
739            if let NodeOrToken::Token(t) = last {
740                if Self::is_whitespace_token(t) {
741                    children.pop();
742                } else {
743                    break;
744                }
745            } else {
746                break;
747            }
748        }
749        children
750    }
751
752    /// Helper to strip trailing whitespace from a RELATION node's children
753    fn strip_relation_trailing_ws(relation: &SyntaxNode) -> GreenNode {
754        let children: Vec<_> = relation
755            .children_with_tokens()
756            .map(|c| Self::to_green(&c))
757            .collect();
758        let stripped = Self::strip_trailing_ws_from_children(children);
759        GreenNode::new(relation.kind().into(), stripped)
760    }
761
762    /// Helper to build nodes for insertion with odd syntax
763    fn build_odd_syntax_nodes(
764        before_ws: &str,
765        after_ws: &str,
766    ) -> Vec<NodeOrToken<GreenNode, GreenToken>> {
767        [
768            (!before_ws.is_empty())
769                .then(|| NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), before_ws))),
770            Some(NodeOrToken::Token(GreenToken::new(COMMA.into(), ","))),
771            (!after_ws.is_empty())
772                .then(|| NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), after_ws))),
773        ]
774        .into_iter()
775        .flatten()
776        .collect()
777    }
778
779    /// Detect if relations use odd syntax (whitespace before comma) and return whitespace parts
780    fn detect_odd_syntax(&self) -> Option<(String, String)> {
781        for entry_node in self.entries() {
782            let mut node = entry_node.0.next_sibling_or_token()?;
783
784            // Skip whitespace tokens and collect their text
785            let mut before = String::new();
786            while matches!(node.kind(), WHITESPACE | NEWLINE) {
787                if let NodeOrToken::Token(t) = &node {
788                    before.push_str(t.text());
789                }
790                node = node.next_sibling_or_token()?;
791            }
792
793            // Check if we found a comma after whitespace
794            if node.kind() == COMMA && !before.is_empty() {
795                let after = Self::collect_whitespace(node.next_sibling_or_token());
796                return Some((before, after));
797            }
798        }
799        None
800    }
801
802    /// Detect the most common whitespace pattern after commas in the relations.
803    ///
804    /// This matches debmutate's behavior of analyzing existing whitespace
805    /// patterns to preserve formatting when adding entries.
806    ///
807    /// # Arguments
808    /// * `default` - The default whitespace pattern to use if no pattern is detected
809    fn detect_whitespace_pattern(&self, default: &str) -> String {
810        use std::collections::HashMap;
811
812        let entries: Vec<_> = self.entries().collect();
813        let num_entries = entries.len();
814
815        if num_entries == 0 {
816            return String::from(""); // Empty list - first entry gets no prefix
817        }
818
819        if num_entries == 1 {
820            // Single entry - check if there's a pattern after it
821            if let Some(node) = entries[0].0.next_sibling_or_token() {
822                if node.kind() == COMMA {
823                    let pattern = Self::collect_whitespace(node.next_sibling_or_token());
824                    if !pattern.is_empty() {
825                        return pattern;
826                    }
827                }
828            }
829            return default.to_string(); // Use default for single entry with no pattern
830        }
831
832        // Count whitespace patterns after commas (excluding the last entry)
833        let mut whitespace_counts: HashMap<String, usize> = HashMap::new();
834
835        for (i, entry) in entries.iter().enumerate() {
836            if i == num_entries - 1 {
837                break; // Skip the last entry
838            }
839
840            // Look for comma and whitespace after this entry
841            if let Some(mut node) = entry.0.next_sibling_or_token() {
842                // Skip any whitespace/newlines before the comma (odd syntax)
843                while matches!(node.kind(), WHITESPACE | NEWLINE) {
844                    if let Some(next) = node.next_sibling_or_token() {
845                        node = next;
846                    } else {
847                        break;
848                    }
849                }
850
851                // Found comma, collect all whitespace/newlines after it
852                if node.kind() == COMMA {
853                    let pattern = Self::collect_whitespace(node.next_sibling_or_token());
854                    if !pattern.is_empty() {
855                        *whitespace_counts.entry(pattern).or_insert(0) += 1;
856                    }
857                }
858            }
859        }
860
861        // If there's exactly one pattern, use it
862        if whitespace_counts.len() == 1 {
863            if let Some((ws, _)) = whitespace_counts.iter().next() {
864                return ws.clone();
865            }
866        }
867
868        // Multiple patterns - use the most common
869        if let Some((ws, _)) = whitespace_counts.iter().max_by_key(|(_, count)| *count) {
870            return ws.clone();
871        }
872
873        // Use the provided default
874        default.to_string()
875    }
876
877    /// Insert a new entry at the given index
878    ///
879    /// # Arguments
880    /// * `idx` - The index to insert at
881    /// * `entry` - The entry to insert
882    /// * `default_sep` - Optional default separator to use if no pattern is detected (defaults to " ")
883    pub fn insert_with_separator(&mut self, idx: usize, entry: Entry, default_sep: Option<&str>) {
884        let is_empty = self.entries().next().is_none();
885        let whitespace = self.detect_whitespace_pattern(default_sep.unwrap_or(" "));
886
887        // Strip trailing whitespace first
888        self.strip_trailing_whitespace();
889
890        // Detect odd syntax (whitespace before comma)
891        let odd_syntax = self.detect_odd_syntax();
892
893        let (position, new_children) = if let Some(current_entry) = self.entries().nth(idx) {
894            let to_insert = if idx == 0 && is_empty {
895                vec![entry.0.green().into()]
896            } else if let Some((before_ws, after_ws)) = &odd_syntax {
897                let mut nodes = vec![entry.0.green().into()];
898                nodes.extend(Self::build_odd_syntax_nodes(before_ws, after_ws));
899                nodes
900            } else {
901                vec![
902                    entry.0.green().into(),
903                    NodeOrToken::Token(GreenToken::new(COMMA.into(), ",")),
904                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), whitespace.as_str())),
905                ]
906            };
907
908            (current_entry.0.index(), to_insert)
909        } else {
910            let child_count = self.0.children_with_tokens().count();
911            let to_insert = if idx == 0 {
912                vec![entry.0.green().into()]
913            } else if let Some((before_ws, after_ws)) = &odd_syntax {
914                let mut nodes = Self::build_odd_syntax_nodes(before_ws, after_ws);
915                nodes.push(entry.0.green().into());
916                nodes
917            } else {
918                vec![
919                    NodeOrToken::Token(GreenToken::new(COMMA.into(), ",")),
920                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), whitespace.as_str())),
921                    entry.0.green().into(),
922                ]
923            };
924
925            (child_count, to_insert)
926        };
927        // We can safely replace the root here since Relations is a root node
928        self.0 = SyntaxNode::new_root_mut(
929            self.0.replace_with(
930                self.0
931                    .green()
932                    .splice_children(position..position, new_children),
933            ),
934        );
935    }
936
937    /// Insert a new entry at the given index with default separator
938    pub fn insert(&mut self, idx: usize, entry: Entry) {
939        self.insert_with_separator(idx, entry, None);
940    }
941
942    /// Helper to recursively strip trailing whitespace from an ENTRY node
943    fn strip_entry_trailing_ws(entry: &SyntaxNode) -> GreenNode {
944        let mut children: Vec<_> = entry
945            .children_with_tokens()
946            .map(|c| Self::to_green(&c))
947            .collect();
948
949        // Strip trailing whitespace from the last RELATION if present
950        if let Some(NodeOrToken::Node(last)) = children.last() {
951            if last.kind() == rowan::SyntaxKind(RELATION as u16) {
952                // Replace last child with stripped version
953                let relation_node = entry.children().last().unwrap();
954                children.pop();
955                children.push(NodeOrToken::Node(Self::strip_relation_trailing_ws(
956                    &relation_node,
957                )));
958            }
959        }
960
961        // Strip trailing whitespace tokens at entry level
962        let stripped = Self::strip_trailing_ws_from_children(children);
963        GreenNode::new(ENTRY.into(), stripped)
964    }
965
966    fn strip_trailing_whitespace(&mut self) {
967        let mut children: Vec<_> = self
968            .0
969            .children_with_tokens()
970            .map(|c| Self::to_green(&c))
971            .collect();
972
973        // Strip trailing whitespace from the last ENTRY if present
974        if let Some(NodeOrToken::Node(last)) = children.last() {
975            if last.kind() == rowan::SyntaxKind(ENTRY as u16) {
976                let last_entry = self.0.children().last().unwrap();
977                children.pop();
978                children.push(NodeOrToken::Node(Self::strip_entry_trailing_ws(
979                    &last_entry,
980                )));
981            }
982        }
983
984        // Strip trailing whitespace tokens at root level
985        let stripped = Self::strip_trailing_ws_from_children(children);
986
987        let nc = self.0.children_with_tokens().count();
988        self.0 = SyntaxNode::new_root_mut(
989            self.0
990                .replace_with(self.0.green().splice_children(0..nc, stripped)),
991        );
992    }
993
994    /// Replace the entry at the given index
995    pub fn replace(&mut self, idx: usize, entry: Entry) {
996        let current_entry = self.get_entry(idx).unwrap();
997        self.0.splice_children(
998            current_entry.0.index()..current_entry.0.index() + 1,
999            vec![entry.0.into()],
1000        );
1001    }
1002
1003    /// Push a new entry to the relations field
1004    pub fn push(&mut self, entry: Entry) {
1005        let pos = self.entries().count();
1006        self.insert(pos, entry);
1007    }
1008
1009    /// Return the names of substvars in this relations field
1010    pub fn substvars(&self) -> impl Iterator<Item = String> + '_ {
1011        self.0
1012            .children()
1013            .filter_map(Substvar::cast)
1014            .map(|s| s.to_string())
1015    }
1016
1017    /// Parse a relations field from a string, allowing syntax errors
1018    pub fn parse_relaxed(s: &str, allow_substvar: bool) -> (Relations, Vec<String>) {
1019        let parse = parse(s, allow_substvar);
1020        (parse.root_mut(), parse.errors)
1021    }
1022
1023    /// Check if this relations field is satisfied by the given package versions.
1024    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
1025        self.entries().all(|e| e.satisfied_by(package_version))
1026    }
1027
1028    /// Check if this relations field is empty
1029    pub fn is_empty(&self) -> bool {
1030        self.entries().count() == 0
1031    }
1032
1033    /// Get the number of entries in this relations field
1034    pub fn len(&self) -> usize {
1035        self.entries().count()
1036    }
1037
1038    /// Ensure that a package has at least a minimum version constraint.
1039    ///
1040    /// If the package already exists with a version constraint that satisfies
1041    /// the minimum version, it is left unchanged. Otherwise, the constraint
1042    /// is updated or added.
1043    ///
1044    /// # Arguments
1045    /// * `package` - The package name
1046    /// * `minimum_version` - The minimum version required
1047    ///
1048    /// # Example
1049    /// ```
1050    /// use debian_control::lossless::relations::Relations;
1051    ///
1052    /// let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
1053    /// relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
1054    /// assert_eq!(relations.to_string(), "debhelper (>= 12)");
1055    ///
1056    /// let mut relations: Relations = "python3".parse().unwrap();
1057    /// relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
1058    /// assert!(relations.to_string().contains("debhelper (>= 12)"));
1059    /// ```
1060    pub fn ensure_minimum_version(&mut self, package: &str, minimum_version: &Version) {
1061        let mut found = false;
1062        let mut obsolete_indices = vec![];
1063        let mut update_idx = None;
1064
1065        let entries: Vec<_> = self.entries().collect();
1066        for (idx, entry) in entries.iter().enumerate() {
1067            let relations: Vec<_> = entry.relations().collect();
1068
1069            // Check if this entry has multiple alternatives with our package
1070            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1071            if names.len() > 1 && names.contains(&package.to_string()) {
1072                // This is a complex alternative relation, mark for removal if obsolete
1073                let is_obsolete = relations.iter().any(|r| {
1074                    if r.name() != package {
1075                        return false;
1076                    }
1077                    if let Some((vc, ver)) = r.version() {
1078                        matches!(vc, VersionConstraint::GreaterThan if &ver < minimum_version)
1079                            || matches!(vc, VersionConstraint::GreaterThanEqual if &ver <= minimum_version)
1080                    } else {
1081                        false
1082                    }
1083                });
1084                if is_obsolete {
1085                    obsolete_indices.push(idx);
1086                }
1087                continue;
1088            }
1089
1090            // Single package entry
1091            if names.len() == 1 && names[0] == package {
1092                found = true;
1093                let relation = relations.into_iter().next().unwrap();
1094
1095                // Check if update is needed
1096                let should_update = if let Some((vc, ver)) = relation.version() {
1097                    match vc {
1098                        VersionConstraint::GreaterThanEqual | VersionConstraint::GreaterThan => {
1099                            &ver < minimum_version
1100                        }
1101                        _ => false,
1102                    }
1103                } else {
1104                    true
1105                };
1106
1107                if should_update {
1108                    update_idx = Some(idx);
1109                }
1110                break;
1111            }
1112        }
1113
1114        // Perform updates after iteration
1115        if let Some(idx) = update_idx {
1116            let relation = Relation::new(
1117                package,
1118                Some((VersionConstraint::GreaterThanEqual, minimum_version.clone())),
1119            );
1120            // Get the existing entry and replace its relation to preserve formatting
1121            let mut entry = self.get_entry(idx).unwrap();
1122            entry.replace(0, relation);
1123            self.replace(idx, entry);
1124        }
1125
1126        // Remove obsolete entries
1127        for idx in obsolete_indices.into_iter().rev() {
1128            self.remove_entry(idx);
1129        }
1130
1131        // Add if not found
1132        if !found {
1133            let relation = Relation::new(
1134                package,
1135                Some((VersionConstraint::GreaterThanEqual, minimum_version.clone())),
1136            );
1137            self.push(Entry::from(relation));
1138        }
1139    }
1140
1141    /// Ensure that a package has an exact version constraint.
1142    ///
1143    /// # Arguments
1144    /// * `package` - The package name
1145    /// * `version` - The exact version required
1146    ///
1147    /// # Example
1148    /// ```
1149    /// use debian_control::lossless::relations::Relations;
1150    ///
1151    /// let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
1152    /// relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
1153    /// assert_eq!(relations.to_string(), "debhelper (= 12)");
1154    /// ```
1155    pub fn ensure_exact_version(&mut self, package: &str, version: &Version) {
1156        let mut found = false;
1157        let mut update_idx = None;
1158
1159        let entries: Vec<_> = self.entries().collect();
1160        for (idx, entry) in entries.iter().enumerate() {
1161            let relations: Vec<_> = entry.relations().collect();
1162            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1163
1164            if names.len() > 1 && names[0] == package {
1165                panic!("Complex rule for {}, aborting", package);
1166            }
1167
1168            if names.len() == 1 && names[0] == package {
1169                found = true;
1170                let relation = relations.into_iter().next().unwrap();
1171
1172                let should_update = if let Some((vc, ver)) = relation.version() {
1173                    vc != VersionConstraint::Equal || &ver != version
1174                } else {
1175                    true
1176                };
1177
1178                if should_update {
1179                    update_idx = Some(idx);
1180                }
1181                break;
1182            }
1183        }
1184
1185        // Perform update after iteration
1186        if let Some(idx) = update_idx {
1187            let relation =
1188                Relation::new(package, Some((VersionConstraint::Equal, version.clone())));
1189            // Get the existing entry and replace its relation to preserve formatting
1190            let mut entry = self.get_entry(idx).unwrap();
1191            entry.replace(0, relation);
1192            self.replace(idx, entry);
1193        }
1194
1195        if !found {
1196            let relation =
1197                Relation::new(package, Some((VersionConstraint::Equal, version.clone())));
1198            self.push(Entry::from(relation));
1199        }
1200    }
1201
1202    /// Ensure that a package dependency exists, without specifying a version.
1203    ///
1204    /// If the package already exists (with or without a version constraint),
1205    /// the relations field is left unchanged. Otherwise, the package is added
1206    /// without a version constraint.
1207    ///
1208    /// # Arguments
1209    /// * `package` - The package name
1210    ///
1211    /// # Example
1212    /// ```
1213    /// use debian_control::lossless::relations::Relations;
1214    ///
1215    /// let mut relations: Relations = "python3".parse().unwrap();
1216    /// relations.ensure_some_version("debhelper");
1217    /// assert!(relations.to_string().contains("debhelper"));
1218    /// ```
1219    pub fn ensure_some_version(&mut self, package: &str) {
1220        for entry in self.entries() {
1221            let relations: Vec<_> = entry.relations().collect();
1222            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1223
1224            if names.len() > 1 && names[0] == package {
1225                panic!("Complex rule for {}, aborting", package);
1226            }
1227
1228            if names.len() == 1 && names[0] == package {
1229                // Package already exists, don't modify
1230                return;
1231            }
1232        }
1233
1234        // Package not found, add it
1235        let relation = Relation::simple(package);
1236        self.push(Entry::from(relation));
1237    }
1238
1239    /// Ensure that a substitution variable is present in the relations.
1240    ///
1241    /// If the substvar already exists, it is left unchanged. Otherwise, it is added
1242    /// at the end of the relations list.
1243    ///
1244    /// # Arguments
1245    /// * `substvar` - The substitution variable (e.g., "${misc:Depends}")
1246    ///
1247    /// # Returns
1248    /// `Ok(())` on success, or `Err` with an error message if parsing fails
1249    ///
1250    /// # Example
1251    /// ```
1252    /// use debian_control::lossless::relations::Relations;
1253    ///
1254    /// let mut relations: Relations = "python3".parse().unwrap();
1255    /// relations.ensure_substvar("${misc:Depends}").unwrap();
1256    /// assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
1257    /// ```
1258    pub fn ensure_substvar(&mut self, substvar: &str) -> Result<(), String> {
1259        // Check if the substvar already exists
1260        for existing in self.substvars() {
1261            if existing.trim() == substvar.trim() {
1262                return Ok(());
1263            }
1264        }
1265
1266        // Parse the substvar
1267        let (parsed, errors) = Relations::parse_relaxed(substvar, true);
1268        if !errors.is_empty() {
1269            return Err(errors.join("\n"));
1270        }
1271
1272        // Detect whitespace pattern to preserve formatting
1273        let whitespace = self.detect_whitespace_pattern(" ");
1274
1275        // Find the substvar node and inject it
1276        for substvar_node in parsed.0.children().filter(|n| n.kind() == SUBSTVAR) {
1277            let has_content = self.entries().next().is_some() || self.substvars().next().is_some();
1278
1279            let mut builder = GreenNodeBuilder::new();
1280            builder.start_node(ROOT.into());
1281
1282            // Copy existing content
1283            for child in self.0.children_with_tokens() {
1284                match child {
1285                    NodeOrToken::Node(n) => inject(&mut builder, n),
1286                    NodeOrToken::Token(t) => builder.token(t.kind().into(), t.text()),
1287                }
1288            }
1289
1290            // Add separator if needed, using detected whitespace pattern
1291            if has_content {
1292                builder.token(COMMA.into(), ",");
1293                builder.token(WHITESPACE.into(), whitespace.as_str());
1294            }
1295
1296            // Inject the substvar node
1297            inject(&mut builder, substvar_node);
1298
1299            builder.finish_node();
1300            self.0 = SyntaxNode::new_root_mut(builder.finish());
1301        }
1302
1303        Ok(())
1304    }
1305
1306    /// Remove a substitution variable from the relations.
1307    ///
1308    /// If the substvar exists, it is removed along with its surrounding separators.
1309    /// If the substvar does not exist, this is a no-op.
1310    ///
1311    /// # Arguments
1312    /// * `substvar` - The substitution variable to remove (e.g., "${misc:Depends}")
1313    ///
1314    /// # Example
1315    /// ```
1316    /// use debian_control::lossless::relations::Relations;
1317    ///
1318    /// let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
1319    /// relations.drop_substvar("${misc:Depends}");
1320    /// assert_eq!(relations.to_string(), "python3");
1321    /// ```
1322    pub fn drop_substvar(&mut self, substvar: &str) {
1323        // Find all substvar nodes that match the given string
1324        let substvars_to_remove: Vec<_> = self
1325            .0
1326            .children()
1327            .filter_map(Substvar::cast)
1328            .filter(|s| s.to_string().trim() == substvar.trim())
1329            .collect();
1330
1331        for substvar_node in substvars_to_remove {
1332            // Determine if this is the first substvar (no previous ENTRY or SUBSTVAR siblings)
1333            let is_first = !substvar_node
1334                .0
1335                .siblings(Direction::Prev)
1336                .skip(1)
1337                .any(|n| n.kind() == ENTRY || n.kind() == SUBSTVAR);
1338
1339            let mut removed_comma = false;
1340
1341            // Remove whitespace and comma after the substvar
1342            while let Some(n) = substvar_node.0.next_sibling_or_token() {
1343                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1344                    n.detach();
1345                } else if n.kind() == COMMA {
1346                    n.detach();
1347                    removed_comma = true;
1348                    break;
1349                } else {
1350                    break;
1351                }
1352            }
1353
1354            // If not first, remove preceding whitespace and comma
1355            if !is_first {
1356                while let Some(n) = substvar_node.0.prev_sibling_or_token() {
1357                    if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1358                        n.detach();
1359                    } else if !removed_comma && n.kind() == COMMA {
1360                        n.detach();
1361                        break;
1362                    } else {
1363                        break;
1364                    }
1365                }
1366            } else {
1367                // If first and we didn't remove a comma after, clean up any leading whitespace
1368                while let Some(n) = substvar_node.0.next_sibling_or_token() {
1369                    if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1370                        n.detach();
1371                    } else {
1372                        break;
1373                    }
1374                }
1375            }
1376
1377            // Finally, detach the substvar node itself
1378            substvar_node.0.detach();
1379        }
1380    }
1381
1382    /// Filter entries based on a predicate function.
1383    ///
1384    /// # Arguments
1385    /// * `keep` - A function that returns true for entries to keep
1386    ///
1387    /// # Example
1388    /// ```
1389    /// use debian_control::lossless::relations::Relations;
1390    ///
1391    /// let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1392    /// relations.filter_entries(|entry| {
1393    ///     entry.relations().any(|r| r.name().starts_with("python"))
1394    /// });
1395    /// assert_eq!(relations.to_string(), "python3");
1396    /// ```
1397    pub fn filter_entries<F>(&mut self, keep: F)
1398    where
1399        F: Fn(&Entry) -> bool,
1400    {
1401        let indices_to_remove: Vec<_> = self
1402            .entries()
1403            .enumerate()
1404            .filter_map(|(idx, entry)| if keep(&entry) { None } else { Some(idx) })
1405            .collect();
1406
1407        // Remove in reverse order to maintain correct indices
1408        for idx in indices_to_remove.into_iter().rev() {
1409            self.remove_entry(idx);
1410        }
1411    }
1412
1413    /// Check whether the relations are sorted according to a given sorting order.
1414    ///
1415    /// # Arguments
1416    /// * `sorting_order` - The sorting order to check against
1417    ///
1418    /// # Example
1419    /// ```
1420    /// use debian_control::lossless::relations::{Relations, WrapAndSortOrder};
1421    ///
1422    /// let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
1423    /// assert!(relations.is_sorted(&WrapAndSortOrder));
1424    ///
1425    /// let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
1426    /// assert!(!relations.is_sorted(&WrapAndSortOrder));
1427    /// ```
1428    pub fn is_sorted(&self, sorting_order: &impl SortingOrder) -> bool {
1429        let mut last_name: Option<String> = None;
1430        for entry in self.entries() {
1431            // Skip empty entries
1432            let mut relations = entry.relations();
1433            let Some(relation) = relations.next() else {
1434                continue;
1435            };
1436
1437            let name = relation.name();
1438
1439            // Skip items that should be ignored
1440            if sorting_order.ignore(&name) {
1441                continue;
1442            }
1443
1444            // Check if this breaks the sort order
1445            if let Some(ref last) = last_name {
1446                if sorting_order.lt(&name, last) {
1447                    return false;
1448                }
1449            }
1450
1451            last_name = Some(name);
1452        }
1453        true
1454    }
1455
1456    /// Find the position to insert an entry while maintaining sort order.
1457    ///
1458    /// This method detects the current sorting order and returns the appropriate
1459    /// insertion position. If there are fewer than 2 entries, it defaults to
1460    /// WrapAndSortOrder. If no sorting order is detected, it returns the end position.
1461    ///
1462    /// # Arguments
1463    /// * `entry` - The entry to insert
1464    ///
1465    /// # Returns
1466    /// The index where the entry should be inserted
1467    fn find_insert_position(&self, entry: &Entry) -> usize {
1468        // Get the package name from the first relation in the entry
1469        let Some(relation) = entry.relations().next() else {
1470            // Empty entry, just append at the end
1471            return self.len();
1472        };
1473        let package_name = relation.name();
1474
1475        // Count non-empty entries
1476        let count = self.entries().filter(|e| !e.is_empty()).count();
1477
1478        // If there are less than 2 items, default to WrapAndSortOrder
1479        let sorting_order: Box<dyn SortingOrder> = if count < 2 {
1480            Box::new(WrapAndSortOrder)
1481        } else {
1482            // Try to detect which sorting order is being used
1483            // Try WrapAndSortOrder first, then DefaultSortingOrder
1484            if self.is_sorted(&WrapAndSortOrder) {
1485                Box::new(WrapAndSortOrder)
1486            } else if self.is_sorted(&DefaultSortingOrder) {
1487                Box::new(DefaultSortingOrder)
1488            } else {
1489                // No sorting order detected, just append at the end
1490                return self.len();
1491            }
1492        };
1493
1494        // If adding a special item that should be ignored by this sort order, append at the end
1495        if sorting_order.ignore(&package_name) {
1496            return self.len();
1497        }
1498
1499        // Insert in sorted order among regular items
1500        let mut position = 0;
1501        for (idx, existing_entry) in self.entries().enumerate() {
1502            let mut existing_relations = existing_entry.relations();
1503            let Some(existing_relation) = existing_relations.next() else {
1504                // Empty entry, skip
1505                position += 1;
1506                continue;
1507            };
1508
1509            let existing_name = existing_relation.name();
1510
1511            // Skip special items when finding insertion position
1512            if sorting_order.ignore(&existing_name) {
1513                position += 1;
1514                continue;
1515            }
1516
1517            // Compare with regular items only
1518            if sorting_order.lt(&package_name, &existing_name) {
1519                return idx;
1520            }
1521            position += 1;
1522        }
1523
1524        position
1525    }
1526
1527    /// Drop a dependency from the relations by package name.
1528    ///
1529    /// # Arguments
1530    /// * `package` - The package name to remove
1531    ///
1532    /// # Returns
1533    /// `true` if the package was found and removed, `false` otherwise
1534    ///
1535    /// # Example
1536    /// ```
1537    /// use debian_control::lossless::relations::Relations;
1538    ///
1539    /// let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1540    /// assert!(relations.drop_dependency("debhelper"));
1541    /// assert_eq!(relations.to_string(), "python3, rustc");
1542    /// assert!(!relations.drop_dependency("nonexistent"));
1543    /// ```
1544    pub fn drop_dependency(&mut self, package: &str) -> bool {
1545        let indices_to_remove: Vec<_> = self
1546            .entries()
1547            .enumerate()
1548            .filter_map(|(idx, entry)| {
1549                let relations: Vec<_> = entry.relations().collect();
1550                let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1551                if names == vec![package] {
1552                    Some(idx)
1553                } else {
1554                    None
1555                }
1556            })
1557            .collect();
1558
1559        let found = !indices_to_remove.is_empty();
1560
1561        // Remove in reverse order to maintain correct indices
1562        for idx in indices_to_remove.into_iter().rev() {
1563            self.remove_entry(idx);
1564        }
1565
1566        found
1567    }
1568
1569    /// Add a dependency at a specific position or auto-detect the position.
1570    ///
1571    /// If `position` is `None`, the position is automatically determined based
1572    /// on the detected sorting order. If a sorting order is detected, the entry
1573    /// is inserted in the appropriate position to maintain that order. Otherwise,
1574    /// it is appended at the end.
1575    ///
1576    /// # Arguments
1577    /// * `entry` - The entry to add
1578    /// * `position` - Optional position to insert at
1579    ///
1580    /// # Example
1581    /// ```
1582    /// use debian_control::lossless::relations::{Relations, Relation, Entry};
1583    ///
1584    /// let mut relations: Relations = "python3, rustc".parse().unwrap();
1585    /// let entry = Entry::from(Relation::simple("debhelper"));
1586    /// relations.add_dependency(entry, None);
1587    /// // debhelper is inserted in sorted order (if order is detected)
1588    /// ```
1589    pub fn add_dependency(&mut self, entry: Entry, position: Option<usize>) {
1590        let pos = position.unwrap_or_else(|| self.find_insert_position(&entry));
1591        self.insert(pos, entry);
1592    }
1593
1594    /// Get the entry containing a specific package.
1595    ///
1596    /// This returns the first entry that contains exactly one relation with the
1597    /// specified package name (no alternatives).
1598    ///
1599    /// # Arguments
1600    /// * `package` - The package name to search for
1601    ///
1602    /// # Returns
1603    /// A tuple of (index, Entry) if found
1604    ///
1605    /// # Errors
1606    /// Returns `Err` with a message if the package is found in a complex rule (with alternatives)
1607    ///
1608    /// # Example
1609    /// ```
1610    /// use debian_control::lossless::relations::Relations;
1611    ///
1612    /// let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
1613    /// let (idx, entry) = relations.get_relation("debhelper").unwrap();
1614    /// assert_eq!(idx, 1);
1615    /// assert_eq!(entry.to_string(), "debhelper (>= 12)");
1616    /// ```
1617    pub fn get_relation(&self, package: &str) -> Result<(usize, Entry), String> {
1618        for (idx, entry) in self.entries().enumerate() {
1619            let relations: Vec<_> = entry.relations().collect();
1620            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1621
1622            if names.len() > 1 && names.contains(&package.to_string()) {
1623                return Err(format!("Complex rule for {}, aborting", package));
1624            }
1625
1626            if names.len() == 1 && names[0] == package {
1627                return Ok((idx, entry));
1628            }
1629        }
1630        Err(format!("Package {} not found", package))
1631    }
1632
1633    /// Iterate over all entries containing a specific package.
1634    ///
1635    /// # Arguments
1636    /// * `package` - The package name to search for
1637    ///
1638    /// # Returns
1639    /// An iterator over tuples of (index, Entry)
1640    ///
1641    /// # Example
1642    /// ```
1643    /// use debian_control::lossless::relations::Relations;
1644    ///
1645    /// let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
1646    /// let entries: Vec<_> = relations.iter_relations_for("python3").collect();
1647    /// assert_eq!(entries.len(), 1);
1648    /// ```
1649    pub fn iter_relations_for(&self, package: &str) -> impl Iterator<Item = (usize, Entry)> + '_ {
1650        let package = package.to_string();
1651        self.entries().enumerate().filter(move |(_, entry)| {
1652            let names: Vec<_> = entry.relations().map(|r| r.name()).collect();
1653            names.contains(&package)
1654        })
1655    }
1656
1657    /// Check whether a package exists in the relations.
1658    ///
1659    /// # Arguments
1660    /// * `package` - The package name to search for
1661    ///
1662    /// # Returns
1663    /// `true` if the package is found, `false` otherwise
1664    ///
1665    /// # Example
1666    /// ```
1667    /// use debian_control::lossless::relations::Relations;
1668    ///
1669    /// let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1670    /// assert!(relations.has_relation("debhelper"));
1671    /// assert!(!relations.has_relation("nonexistent"));
1672    /// ```
1673    pub fn has_relation(&self, package: &str) -> bool {
1674        self.entries()
1675            .any(|entry| entry.relations().any(|r| r.name() == package))
1676    }
1677}
1678
1679impl From<Vec<Entry>> for Relations {
1680    fn from(entries: Vec<Entry>) -> Self {
1681        let mut builder = GreenNodeBuilder::new();
1682        builder.start_node(ROOT.into());
1683        for (i, entry) in entries.into_iter().enumerate() {
1684            if i > 0 {
1685                builder.token(COMMA.into(), ",");
1686                builder.token(WHITESPACE.into(), " ");
1687            }
1688            inject(&mut builder, entry.0);
1689        }
1690        builder.finish_node();
1691        Relations(SyntaxNode::new_root_mut(builder.finish()))
1692    }
1693}
1694
1695impl From<Entry> for Relations {
1696    fn from(entry: Entry) -> Self {
1697        Self::from(vec![entry])
1698    }
1699}
1700
1701impl Default for Entry {
1702    fn default() -> Self {
1703        Self::new()
1704    }
1705}
1706
1707impl PartialOrd for Entry {
1708    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1709        let mut rels_a = self.relations();
1710        let mut rels_b = other.relations();
1711        while let (Some(a), Some(b)) = (rels_a.next(), rels_b.next()) {
1712            match a.cmp(&b) {
1713                std::cmp::Ordering::Equal => continue,
1714                x => return Some(x),
1715            }
1716        }
1717
1718        if rels_a.next().is_some() {
1719            return Some(std::cmp::Ordering::Greater);
1720        }
1721
1722        if rels_b.next().is_some() {
1723            return Some(std::cmp::Ordering::Less);
1724        }
1725
1726        Some(std::cmp::Ordering::Equal)
1727    }
1728}
1729
1730impl Eq for Entry {}
1731
1732impl Ord for Entry {
1733    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1734        self.partial_cmp(other).unwrap()
1735    }
1736}
1737
1738impl Entry {
1739    /// Create a new entry
1740    pub fn new() -> Self {
1741        let mut builder = GreenNodeBuilder::new();
1742        builder.start_node(SyntaxKind::ENTRY.into());
1743        builder.finish_node();
1744        Entry(SyntaxNode::new_root_mut(builder.finish()))
1745    }
1746
1747    /// Replace the relation at the given index
1748    pub fn replace(&mut self, idx: usize, relation: Relation) {
1749        let current_relation = self.get_relation(idx).unwrap();
1750
1751        let old_root = current_relation.0;
1752        let new_root = relation.0;
1753        // Preserve white the current relation has
1754        let mut prev = new_root.first_child_or_token();
1755        let mut new_head_len = 0;
1756        // First, strip off any whitespace from the new relation
1757        while let Some(p) = prev {
1758            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1759                new_head_len += 1;
1760                prev = p.next_sibling_or_token();
1761            } else {
1762                break;
1763            }
1764        }
1765        let mut new_tail_len = 0;
1766        let mut next = new_root.last_child_or_token();
1767        while let Some(n) = next {
1768            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1769                new_tail_len += 1;
1770                next = n.prev_sibling_or_token();
1771            } else {
1772                break;
1773            }
1774        }
1775        // Then, inherit the whitespace from the old relation
1776        let mut prev = old_root.first_child_or_token();
1777        let mut old_head = vec![];
1778        while let Some(p) = prev {
1779            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1780                old_head.push(p.clone());
1781                prev = p.next_sibling_or_token();
1782            } else {
1783                break;
1784            }
1785        }
1786        let mut old_tail = vec![];
1787        let mut next = old_root.last_child_or_token();
1788        while let Some(n) = next {
1789            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1790                old_tail.push(n.clone());
1791                next = n.prev_sibling_or_token();
1792            } else {
1793                break;
1794            }
1795        }
1796        new_root.splice_children(0..new_head_len, old_head);
1797        let tail_pos = new_root.children_with_tokens().count() - new_tail_len;
1798        new_root.splice_children(
1799            tail_pos - new_tail_len..tail_pos,
1800            old_tail.into_iter().rev(),
1801        );
1802        let index = old_root.index();
1803        self.0
1804            .splice_children(index..index + 1, vec![new_root.into()]);
1805    }
1806
1807    /// Wrap and sort the relations in this entry
1808    #[must_use]
1809    pub fn wrap_and_sort(&self) -> Self {
1810        let mut relations = self
1811            .relations()
1812            .map(|r| r.wrap_and_sort())
1813            .collect::<Vec<_>>();
1814        // TODO: preserve comments
1815        relations.sort();
1816        Self::from(relations)
1817    }
1818
1819    /// Iterate over the relations in this entry
1820    pub fn relations(&self) -> impl Iterator<Item = Relation> + '_ {
1821        self.0.children().filter_map(Relation::cast)
1822    }
1823
1824    /// Iterate over the relations in this entry
1825    pub fn iter(&self) -> impl Iterator<Item = Relation> + '_ {
1826        self.relations()
1827    }
1828
1829    /// Get the relation at the given index
1830    pub fn get_relation(&self, idx: usize) -> Option<Relation> {
1831        self.relations().nth(idx)
1832    }
1833
1834    /// Remove the relation at the given index
1835    ///
1836    /// # Arguments
1837    /// * `idx` - The index of the relation to remove
1838    ///
1839    /// # Example
1840    /// ```
1841    /// use debian_control::lossless::relations::{Relation,Entry};
1842    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-requests".parse().unwrap();
1843    /// entry.remove_relation(1);
1844    /// assert_eq!(entry.to_string(), "python3-dulwich (>= 0.19.0)");
1845    /// ```
1846    pub fn remove_relation(&self, idx: usize) -> Relation {
1847        let mut relation = self.get_relation(idx).unwrap();
1848        relation.remove();
1849        relation
1850    }
1851
1852    /// Check if this entry is satisfied by the given package versions.
1853    ///
1854    /// # Arguments
1855    /// * `package_version` - A function that returns the version of a package.
1856    ///
1857    /// # Example
1858    /// ```
1859    /// use debian_control::lossless::relations::{Relation,Entry};
1860    /// let entry = Entry::from(vec!["samba (>= 2.0)".parse::<Relation>().unwrap()]);
1861    /// assert!(entry.satisfied_by(|name: &str| -> Option<debversion::Version> {
1862    ///    match name {
1863    ///    "samba" => Some("2.0".parse().unwrap()),
1864    ///    _ => None
1865    /// }}));
1866    /// ```
1867    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
1868        self.relations().any(|r| {
1869            let actual = package_version.lookup_version(r.name().as_str());
1870            if let Some((vc, version)) = r.version() {
1871                if let Some(actual) = actual {
1872                    match vc {
1873                        VersionConstraint::GreaterThanEqual => *actual >= version,
1874                        VersionConstraint::LessThanEqual => *actual <= version,
1875                        VersionConstraint::Equal => *actual == version,
1876                        VersionConstraint::GreaterThan => *actual > version,
1877                        VersionConstraint::LessThan => *actual < version,
1878                    }
1879                } else {
1880                    false
1881                }
1882            } else {
1883                actual.is_some()
1884            }
1885        })
1886    }
1887
1888    /// Remove this entry
1889    ///
1890    /// # Example
1891    /// ```
1892    /// use debian_control::lossless::relations::{Relations,Entry};
1893    /// let mut relations: Relations = r"python3-dulwich (>= 0.19.0), python3-urllib3 (<< 1.26.0)".parse().unwrap();
1894    /// let mut entry = relations.get_entry(0).unwrap();
1895    /// entry.remove();
1896    /// assert_eq!(relations.to_string(), "python3-urllib3 (<< 1.26.0)");
1897    /// ```
1898    pub fn remove(&mut self) {
1899        let mut removed_comma = false;
1900        let is_first = !self
1901            .0
1902            .siblings(Direction::Prev)
1903            .skip(1)
1904            .any(|n| n.kind() == ENTRY);
1905        while let Some(n) = self.0.next_sibling_or_token() {
1906            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1907                n.detach();
1908            } else if n.kind() == COMMA {
1909                n.detach();
1910                removed_comma = true;
1911                break;
1912            } else {
1913                panic!("Unexpected node: {:?}", n);
1914            }
1915        }
1916        if !is_first {
1917            while let Some(n) = self.0.prev_sibling_or_token() {
1918                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1919                    n.detach();
1920                } else if !removed_comma && n.kind() == COMMA {
1921                    n.detach();
1922                    break;
1923                } else {
1924                    break;
1925                }
1926            }
1927        } else {
1928            while let Some(n) = self.0.next_sibling_or_token() {
1929                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1930                    n.detach();
1931                } else {
1932                    break;
1933                }
1934            }
1935        }
1936        self.0.detach();
1937    }
1938
1939    /// Check if this entry is empty
1940    pub fn is_empty(&self) -> bool {
1941        self.relations().count() == 0
1942    }
1943
1944    /// Get the number of relations in this entry
1945    pub fn len(&self) -> usize {
1946        self.relations().count()
1947    }
1948
1949    /// Push a new relation to the entry
1950    ///
1951    /// # Arguments
1952    /// * `relation` - The relation to push
1953    ///
1954    /// # Example
1955    /// ```
1956    /// use debian_control::lossless::relations::{Relation,Entry};
1957    /// let mut entry: Entry = "samba (>= 2.0)".parse().unwrap();
1958    /// entry.push("python3-requests".parse().unwrap());
1959    /// assert_eq!(entry.to_string(), "samba (>= 2.0) | python3-requests");
1960    /// ```
1961    pub fn push(&mut self, relation: Relation) {
1962        let is_empty = !self
1963            .0
1964            .children_with_tokens()
1965            .any(|n| n.kind() == PIPE || n.kind() == RELATION);
1966
1967        let (position, new_children) = if let Some(current_relation) = self.relations().last() {
1968            let to_insert: Vec<NodeOrToken<GreenNode, GreenToken>> = if is_empty {
1969                vec![relation.0.green().into()]
1970            } else {
1971                vec![
1972                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
1973                    NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
1974                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
1975                    relation.0.green().into(),
1976                ]
1977            };
1978
1979            (current_relation.0.index() + 1, to_insert)
1980        } else {
1981            let child_count = self.0.children_with_tokens().count();
1982            (
1983                child_count,
1984                if is_empty {
1985                    vec![relation.0.green().into()]
1986                } else {
1987                    vec![
1988                        NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
1989                        NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
1990                        relation.0.green().into(),
1991                    ]
1992                },
1993            )
1994        };
1995
1996        let new_root = SyntaxNode::new_root_mut(
1997            self.0.replace_with(
1998                self.0
1999                    .green()
2000                    .splice_children(position..position, new_children),
2001            ),
2002        );
2003
2004        if let Some(parent) = self.0.parent() {
2005            parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2006            self.0 = parent
2007                .children_with_tokens()
2008                .nth(self.0.index())
2009                .unwrap()
2010                .clone()
2011                .into_node()
2012                .unwrap();
2013        } else {
2014            self.0 = new_root;
2015        }
2016    }
2017
2018    /// Check if this entry (OR-group) is implied by another entry.
2019    ///
2020    /// An entry is implied by another if any of the relations in this entry
2021    /// is implied by any relation in the outer entry. This follows the semantics
2022    /// of OR-groups in Debian dependencies.
2023    ///
2024    /// For example:
2025    /// - `pkg >= 1.0` is implied by `pkg >= 1.5 | libc6` (first relation matches)
2026    /// - `pkg1 | pkg2` is implied by `pkg1` (pkg1 satisfies the requirement)
2027    ///
2028    /// # Arguments
2029    /// * `outer` - The outer entry that may imply this entry
2030    ///
2031    /// # Returns
2032    /// `true` if this entry is implied by `outer`, `false` otherwise
2033    ///
2034    /// # Example
2035    /// ```
2036    /// use debian_control::lossless::relations::Entry;
2037    ///
2038    /// let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
2039    /// let outer: Entry = "pkg (>= 1.5) | libc6".parse().unwrap();
2040    /// assert!(inner.is_implied_by(&outer));
2041    /// ```
2042    pub fn is_implied_by(&self, outer: &Entry) -> bool {
2043        // If entries are identical, they imply each other
2044        if self == outer {
2045            return true;
2046        }
2047
2048        // Check if any relation in inner is implied by any relation in outer
2049        for inner_rel in self.relations() {
2050            for outer_rel in outer.relations() {
2051                if inner_rel.is_implied_by(&outer_rel) {
2052                    return true;
2053                }
2054            }
2055        }
2056
2057        false
2058    }
2059}
2060
2061fn inject(builder: &mut GreenNodeBuilder, node: SyntaxNode) {
2062    builder.start_node(node.kind().into());
2063    for child in node.children_with_tokens() {
2064        match child {
2065            rowan::NodeOrToken::Node(child) => {
2066                inject(builder, child);
2067            }
2068            rowan::NodeOrToken::Token(token) => {
2069                builder.token(token.kind().into(), token.text());
2070            }
2071        }
2072    }
2073    builder.finish_node();
2074}
2075
2076impl From<Vec<Relation>> for Entry {
2077    fn from(relations: Vec<Relation>) -> Self {
2078        let mut builder = GreenNodeBuilder::new();
2079        builder.start_node(SyntaxKind::ENTRY.into());
2080        for (i, relation) in relations.into_iter().enumerate() {
2081            if i > 0 {
2082                builder.token(WHITESPACE.into(), " ");
2083                builder.token(COMMA.into(), "|");
2084                builder.token(WHITESPACE.into(), " ");
2085            }
2086            inject(&mut builder, relation.0);
2087        }
2088        builder.finish_node();
2089        Entry(SyntaxNode::new_root_mut(builder.finish()))
2090    }
2091}
2092
2093impl From<Relation> for Entry {
2094    fn from(relation: Relation) -> Self {
2095        Self::from(vec![relation])
2096    }
2097}
2098
2099/// Helper function to tokenize a version string, handling epochs
2100/// Version strings like "1:2.3.2-2~" need to be split into: IDENT("1"), COLON, IDENT("2.3.2-2~")
2101fn tokenize_version(builder: &mut GreenNodeBuilder, version: &Version) {
2102    let version_str = version.to_string();
2103
2104    // Split on the first colon (if any) to handle epochs
2105    if let Some(colon_pos) = version_str.find(':') {
2106        // Epoch part (before colon)
2107        builder.token(IDENT.into(), &version_str[..colon_pos]);
2108        builder.token(COLON.into(), ":");
2109        // Version part (after colon)
2110        builder.token(IDENT.into(), &version_str[colon_pos + 1..]);
2111    } else {
2112        // No epoch, just a regular version
2113        builder.token(IDENT.into(), version_str.as_str());
2114    }
2115}
2116
2117impl Relation {
2118    /// Create a new relation
2119    ///
2120    /// # Arguments
2121    /// * `name` - The name of the package
2122    /// * `version_constraint` - The version constraint and version to use
2123    ///
2124    /// # Example
2125    /// ```
2126    /// use debian_control::lossless::relations::{Relation};
2127    /// use debian_control::relations::VersionConstraint;
2128    /// let relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2129    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2130    /// ```
2131    pub fn new(name: &str, version_constraint: Option<(VersionConstraint, Version)>) -> Self {
2132        let mut builder = GreenNodeBuilder::new();
2133        builder.start_node(SyntaxKind::RELATION.into());
2134        builder.token(IDENT.into(), name);
2135        if let Some((vc, version)) = version_constraint {
2136            builder.token(WHITESPACE.into(), " ");
2137            builder.start_node(SyntaxKind::VERSION.into());
2138            builder.token(L_PARENS.into(), "(");
2139            builder.start_node(SyntaxKind::CONSTRAINT.into());
2140            for c in vc.to_string().chars() {
2141                builder.token(
2142                    match c {
2143                        '>' => R_ANGLE.into(),
2144                        '<' => L_ANGLE.into(),
2145                        '=' => EQUAL.into(),
2146                        _ => unreachable!(),
2147                    },
2148                    c.to_string().as_str(),
2149                );
2150            }
2151            builder.finish_node();
2152
2153            builder.token(WHITESPACE.into(), " ");
2154
2155            tokenize_version(&mut builder, &version);
2156
2157            builder.token(R_PARENS.into(), ")");
2158
2159            builder.finish_node();
2160        }
2161
2162        builder.finish_node();
2163        Relation(SyntaxNode::new_root_mut(builder.finish()))
2164    }
2165
2166    /// Wrap and sort this relation
2167    ///
2168    /// # Example
2169    /// ```
2170    /// use debian_control::lossless::relations::Relation;
2171    /// let relation = "  samba  (  >= 2.0) ".parse::<Relation>().unwrap();
2172    /// assert_eq!(relation.wrap_and_sort().to_string(), "samba (>= 2.0)");
2173    /// ```
2174    #[must_use]
2175    pub fn wrap_and_sort(&self) -> Self {
2176        let mut builder = GreenNodeBuilder::new();
2177        builder.start_node(SyntaxKind::RELATION.into());
2178        builder.token(IDENT.into(), self.name().as_str());
2179        if let Some(archqual) = self.archqual() {
2180            builder.token(COLON.into(), ":");
2181            builder.token(IDENT.into(), archqual.as_str());
2182        }
2183        if let Some((vc, version)) = self.version() {
2184            builder.token(WHITESPACE.into(), " ");
2185            builder.start_node(SyntaxKind::VERSION.into());
2186            builder.token(L_PARENS.into(), "(");
2187            builder.start_node(SyntaxKind::CONSTRAINT.into());
2188            builder.token(
2189                match vc {
2190                    VersionConstraint::GreaterThanEqual => R_ANGLE.into(),
2191                    VersionConstraint::LessThanEqual => L_ANGLE.into(),
2192                    VersionConstraint::Equal => EQUAL.into(),
2193                    VersionConstraint::GreaterThan => R_ANGLE.into(),
2194                    VersionConstraint::LessThan => L_ANGLE.into(),
2195                },
2196                vc.to_string().as_str(),
2197            );
2198            builder.finish_node();
2199            builder.token(WHITESPACE.into(), " ");
2200            tokenize_version(&mut builder, &version);
2201            builder.token(R_PARENS.into(), ")");
2202            builder.finish_node();
2203        }
2204        if let Some(architectures) = self.architectures() {
2205            builder.token(WHITESPACE.into(), " ");
2206            builder.start_node(ARCHITECTURES.into());
2207            builder.token(L_BRACKET.into(), "[");
2208            for (i, arch) in architectures.enumerate() {
2209                if i > 0 {
2210                    builder.token(WHITESPACE.into(), " ");
2211                }
2212                builder.token(IDENT.into(), arch.as_str());
2213            }
2214            builder.token(R_BRACKET.into(), "]");
2215            builder.finish_node();
2216        }
2217        for profiles in self.profiles() {
2218            builder.token(WHITESPACE.into(), " ");
2219            builder.start_node(PROFILES.into());
2220            builder.token(L_ANGLE.into(), "<");
2221            for (i, profile) in profiles.into_iter().enumerate() {
2222                if i > 0 {
2223                    builder.token(WHITESPACE.into(), " ");
2224                }
2225                match profile {
2226                    BuildProfile::Disabled(name) => {
2227                        builder.token(NOT.into(), "!");
2228                        builder.token(IDENT.into(), name.as_str());
2229                    }
2230                    BuildProfile::Enabled(name) => {
2231                        builder.token(IDENT.into(), name.as_str());
2232                    }
2233                }
2234            }
2235            builder.token(R_ANGLE.into(), ">");
2236            builder.finish_node();
2237        }
2238        builder.finish_node();
2239        Relation(SyntaxNode::new_root_mut(builder.finish()))
2240    }
2241
2242    /// Create a new simple relation, without any version constraints.
2243    ///
2244    /// # Example
2245    /// ```
2246    /// use debian_control::lossless::relations::Relation;
2247    /// let relation = Relation::simple("samba");
2248    /// assert_eq!(relation.to_string(), "samba");
2249    /// ```
2250    pub fn simple(name: &str) -> Self {
2251        Self::new(name, None)
2252    }
2253
2254    /// Remove the version constraint from the relation.
2255    ///
2256    /// # Example
2257    /// ```
2258    /// use debian_control::lossless::relations::{Relation};
2259    /// use debian_control::relations::VersionConstraint;
2260    /// let mut relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2261    /// relation.drop_constraint();
2262    /// assert_eq!(relation.to_string(), "samba");
2263    /// ```
2264    pub fn drop_constraint(&mut self) -> bool {
2265        let version_token = self.0.children().find(|n| n.kind() == VERSION);
2266        if let Some(version_token) = version_token {
2267            // Remove any whitespace before the version token
2268            while let Some(prev) = version_token.prev_sibling_or_token() {
2269                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2270                    prev.detach();
2271                } else {
2272                    break;
2273                }
2274            }
2275            version_token.detach();
2276            return true;
2277        }
2278
2279        false
2280    }
2281
2282    /// Return the name of the package in the relation.
2283    ///
2284    /// # Example
2285    /// ```
2286    /// use debian_control::lossless::relations::Relation;
2287    /// let relation = Relation::simple("samba");
2288    /// assert_eq!(relation.name(), "samba");
2289    /// ```
2290    pub fn name(&self) -> String {
2291        self.0
2292            .children_with_tokens()
2293            .find_map(|it| match it {
2294                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2295                _ => None,
2296            })
2297            .unwrap()
2298            .text()
2299            .to_string()
2300    }
2301
2302    /// Return the archqual
2303    ///
2304    /// # Example
2305    /// ```
2306    /// use debian_control::lossless::relations::Relation;
2307    /// let relation: Relation = "samba:any".parse().unwrap();
2308    /// assert_eq!(relation.archqual(), Some("any".to_string()));
2309    /// ```
2310    pub fn archqual(&self) -> Option<String> {
2311        let archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2312        let node = if let Some(archqual) = archqual {
2313            archqual.children_with_tokens().find_map(|it| match it {
2314                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2315                _ => None,
2316            })
2317        } else {
2318            None
2319        };
2320        node.map(|n| n.text().to_string())
2321    }
2322
2323    /// Set the architecture qualifier for this relation.
2324    ///
2325    /// # Example
2326    /// ```
2327    /// use debian_control::lossless::relations::Relation;
2328    /// let mut relation = Relation::simple("samba");
2329    /// relation.set_archqual("any");
2330    /// assert_eq!(relation.to_string(), "samba:any");
2331    /// ```
2332    pub fn set_archqual(&mut self, archqual: &str) {
2333        let mut builder = GreenNodeBuilder::new();
2334        builder.start_node(ARCHQUAL.into());
2335        builder.token(COLON.into(), ":");
2336        builder.token(IDENT.into(), archqual);
2337        builder.finish_node();
2338
2339        let node_archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2340        if let Some(node_archqual) = node_archqual {
2341            self.0.splice_children(
2342                node_archqual.index()..node_archqual.index() + 1,
2343                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2344            );
2345        } else {
2346            let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2347            let idx = if let Some(name_node) = name_node {
2348                name_node.index() + 1
2349            } else {
2350                0
2351            };
2352            self.0.splice_children(
2353                idx..idx,
2354                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2355            );
2356        }
2357    }
2358
2359    /// Return the version constraint and the version it is constrained to.
2360    pub fn version(&self) -> Option<(VersionConstraint, Version)> {
2361        let vc = self.0.children().find(|n| n.kind() == VERSION);
2362        let vc = vc.as_ref()?;
2363        let constraint = vc.children().find(|n| n.kind() == CONSTRAINT);
2364
2365        // Collect all IDENT and COLON tokens to handle versions with epochs (e.g., "1:2.3.2-2~")
2366        let version_str: String = vc
2367            .children_with_tokens()
2368            .filter_map(|it| match it {
2369                SyntaxElement::Token(token) if token.kind() == IDENT || token.kind() == COLON => {
2370                    Some(token.text().to_string())
2371                }
2372                _ => None,
2373            })
2374            .collect();
2375
2376        if let Some(constraint) = constraint {
2377            if !version_str.is_empty() {
2378                let vc: VersionConstraint = constraint.to_string().parse().unwrap();
2379                Some((vc, version_str.parse().unwrap()))
2380            } else {
2381                None
2382            }
2383        } else {
2384            None
2385        }
2386    }
2387
2388    /// Set the version constraint for this relation
2389    ///
2390    /// # Example
2391    /// ```
2392    /// use debian_control::lossless::relations::{Relation};
2393    /// use debian_control::relations::VersionConstraint;
2394    /// let mut relation = Relation::simple("samba");
2395    /// relation.set_version(Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2396    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2397    /// ```
2398    pub fn set_version(&mut self, version_constraint: Option<(VersionConstraint, Version)>) {
2399        let current_version = self.0.children().find(|n| n.kind() == VERSION);
2400        if let Some((vc, version)) = version_constraint {
2401            let mut builder = GreenNodeBuilder::new();
2402            builder.start_node(VERSION.into());
2403            builder.token(L_PARENS.into(), "(");
2404            builder.start_node(CONSTRAINT.into());
2405            match vc {
2406                VersionConstraint::GreaterThanEqual => {
2407                    builder.token(R_ANGLE.into(), ">");
2408                    builder.token(EQUAL.into(), "=");
2409                }
2410                VersionConstraint::LessThanEqual => {
2411                    builder.token(L_ANGLE.into(), "<");
2412                    builder.token(EQUAL.into(), "=");
2413                }
2414                VersionConstraint::Equal => {
2415                    builder.token(EQUAL.into(), "=");
2416                }
2417                VersionConstraint::GreaterThan => {
2418                    builder.token(R_ANGLE.into(), ">");
2419                }
2420                VersionConstraint::LessThan => {
2421                    builder.token(L_ANGLE.into(), "<");
2422                }
2423            }
2424            builder.finish_node(); // CONSTRAINT
2425            builder.token(WHITESPACE.into(), " ");
2426            tokenize_version(&mut builder, &version);
2427            builder.token(R_PARENS.into(), ")");
2428            builder.finish_node(); // VERSION
2429
2430            if let Some(current_version) = current_version {
2431                self.0.splice_children(
2432                    current_version.index()..current_version.index() + 1,
2433                    vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2434                );
2435            } else {
2436                let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2437                let idx = if let Some(name_node) = name_node {
2438                    name_node.index() + 1
2439                } else {
2440                    0
2441                };
2442                let new_children = vec![
2443                    GreenToken::new(WHITESPACE.into(), " ").into(),
2444                    builder.finish().into(),
2445                ];
2446                let new_root = SyntaxNode::new_root_mut(
2447                    self.0.green().splice_children(idx..idx, new_children),
2448                );
2449                if let Some(parent) = self.0.parent() {
2450                    parent
2451                        .splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2452                    self.0 = parent
2453                        .children_with_tokens()
2454                        .nth(self.0.index())
2455                        .unwrap()
2456                        .clone()
2457                        .into_node()
2458                        .unwrap();
2459                } else {
2460                    self.0 = new_root;
2461                }
2462            }
2463        } else if let Some(current_version) = current_version {
2464            // Remove any whitespace before the version token
2465            while let Some(prev) = current_version.prev_sibling_or_token() {
2466                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2467                    prev.detach();
2468                } else {
2469                    break;
2470                }
2471            }
2472            current_version.detach();
2473        }
2474    }
2475
2476    /// Return an iterator over the architectures for this relation
2477    ///
2478    /// # Example
2479    /// ```
2480    /// use debian_control::lossless::relations::Relation;
2481    /// let relation: Relation = "samba [amd64]".parse().unwrap();
2482    /// assert_eq!(relation.architectures().unwrap().collect::<Vec<_>>(), vec!["amd64".to_string()]);
2483    /// ```
2484    pub fn architectures(&self) -> Option<impl Iterator<Item = String> + '_> {
2485        let architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES)?;
2486
2487        Some(architectures.children_with_tokens().filter_map(|node| {
2488            let token = node.as_token()?;
2489            if token.kind() == IDENT {
2490                Some(token.text().to_string())
2491            } else {
2492                None
2493            }
2494        }))
2495    }
2496
2497    /// Returns an iterator over the build profiles for this relation
2498    ///
2499    /// # Example
2500    /// ```
2501    /// use debian_control::lossless::relations::{Relation};
2502    /// use debian_control::relations::{BuildProfile};
2503    /// let relation: Relation = "samba <!nocheck>".parse().unwrap();
2504    /// assert_eq!(relation.profiles().collect::<Vec<_>>(), vec![vec![BuildProfile::Disabled("nocheck".to_string())]]);
2505    /// ```
2506    pub fn profiles(&self) -> impl Iterator<Item = Vec<BuildProfile>> + '_ {
2507        let profiles = self.0.children().filter(|n| n.kind() == PROFILES);
2508
2509        profiles.map(|profile| {
2510            // iterate over nodes separated by whitespace tokens
2511            let mut ret = vec![];
2512            let mut current = vec![];
2513            for token in profile.children_with_tokens() {
2514                match token.kind() {
2515                    WHITESPACE | NEWLINE => {
2516                        if !current.is_empty() {
2517                            ret.push(current.join("").parse::<BuildProfile>().unwrap());
2518                            current = vec![];
2519                        }
2520                    }
2521                    L_ANGLE | R_ANGLE => {}
2522                    _ => {
2523                        current.push(token.to_string());
2524                    }
2525                }
2526            }
2527            if !current.is_empty() {
2528                ret.push(current.concat().parse().unwrap());
2529            }
2530            ret
2531        })
2532    }
2533
2534    /// Remove this relation
2535    ///
2536    /// # Example
2537    /// ```
2538    /// use debian_control::lossless::relations::{Relation,Entry};
2539    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-urllib3 (<< 1.26.0)".parse().unwrap();
2540    /// let mut relation = entry.get_relation(0).unwrap();
2541    /// relation.remove();
2542    /// assert_eq!(entry.to_string(), "python3-urllib3 (<< 1.26.0)");
2543    /// ```
2544    pub fn remove(&mut self) {
2545        let is_first = !self
2546            .0
2547            .siblings(Direction::Prev)
2548            .skip(1)
2549            .any(|n| n.kind() == RELATION);
2550        if !is_first {
2551            // Not the first item in the list. Remove whitespace backwards to the previous
2552            // pipe, the pipe and any whitespace until the previous relation
2553            while let Some(n) = self.0.prev_sibling_or_token() {
2554                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2555                    n.detach();
2556                } else if n.kind() == PIPE {
2557                    n.detach();
2558                    break;
2559                } else {
2560                    break;
2561                }
2562            }
2563            while let Some(n) = self.0.prev_sibling_or_token() {
2564                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2565                    n.detach();
2566                } else {
2567                    break;
2568                }
2569            }
2570        } else {
2571            // First item in the list. Remove whitespace up to the pipe, the pipe and anything
2572            // before the next relation
2573            while let Some(n) = self.0.next_sibling_or_token() {
2574                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2575                    n.detach();
2576                } else if n.kind() == PIPE {
2577                    n.detach();
2578                    break;
2579                } else {
2580                    panic!("Unexpected node: {:?}", n);
2581                }
2582            }
2583
2584            while let Some(n) = self.0.next_sibling_or_token() {
2585                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2586                    n.detach();
2587                } else {
2588                    break;
2589                }
2590            }
2591        }
2592        // If this was the last relation in the entry, remove the entire entry
2593        if let Some(mut parent) = self.0.parent().and_then(Entry::cast) {
2594            if parent.is_empty() {
2595                parent.remove();
2596            } else {
2597                self.0.detach();
2598            }
2599        } else {
2600            self.0.detach();
2601        }
2602    }
2603
2604    /// Set the architectures for this relation
2605    ///
2606    /// # Example
2607    /// ```
2608    /// use debian_control::lossless::relations::Relation;
2609    /// let mut relation = Relation::simple("samba");
2610    /// relation.set_architectures(vec!["amd64", "i386"].into_iter());
2611    /// assert_eq!(relation.to_string(), "samba [amd64 i386]");
2612    /// ```
2613    pub fn set_architectures<'a>(&mut self, architectures: impl Iterator<Item = &'a str>) {
2614        let mut builder = GreenNodeBuilder::new();
2615        builder.start_node(ARCHITECTURES.into());
2616        builder.token(L_BRACKET.into(), "[");
2617        for (i, arch) in architectures.enumerate() {
2618            if i > 0 {
2619                builder.token(WHITESPACE.into(), " ");
2620            }
2621            builder.token(IDENT.into(), arch);
2622        }
2623        builder.token(R_BRACKET.into(), "]");
2624        builder.finish_node();
2625
2626        let node_architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES);
2627        if let Some(node_architectures) = node_architectures {
2628            let new_root = SyntaxNode::new_root_mut(builder.finish());
2629            self.0.splice_children(
2630                node_architectures.index()..node_architectures.index() + 1,
2631                vec![new_root.into()],
2632            );
2633        } else {
2634            let profiles = self.0.children().find(|n| n.kind() == PROFILES);
2635            let idx = if let Some(profiles) = profiles {
2636                profiles.index()
2637            } else {
2638                self.0.children_with_tokens().count()
2639            };
2640            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2641                idx..idx,
2642                vec![
2643                    GreenToken::new(WHITESPACE.into(), " ").into(),
2644                    builder.finish().into(),
2645                ],
2646            ));
2647            if let Some(parent) = self.0.parent() {
2648                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2649                self.0 = parent
2650                    .children_with_tokens()
2651                    .nth(self.0.index())
2652                    .unwrap()
2653                    .clone()
2654                    .into_node()
2655                    .unwrap();
2656            } else {
2657                self.0 = new_root;
2658            }
2659        }
2660    }
2661
2662    /// Add a build profile to this relation
2663    ///
2664    /// # Example
2665    /// ```
2666    /// use debian_control::lossless::relations::Relation;
2667    /// use debian_control::relations::BuildProfile;
2668    /// let mut relation = Relation::simple("samba");
2669    /// relation.add_profile(&[BuildProfile::Disabled("nocheck".to_string())]);
2670    /// assert_eq!(relation.to_string(), "samba <!nocheck>");
2671    /// ```
2672    pub fn add_profile(&mut self, profile: &[BuildProfile]) {
2673        let mut builder = GreenNodeBuilder::new();
2674        builder.start_node(PROFILES.into());
2675        builder.token(L_ANGLE.into(), "<");
2676        for (i, profile) in profile.iter().enumerate() {
2677            if i > 0 {
2678                builder.token(WHITESPACE.into(), " ");
2679            }
2680            match profile {
2681                BuildProfile::Disabled(name) => {
2682                    builder.token(NOT.into(), "!");
2683                    builder.token(IDENT.into(), name.as_str());
2684                }
2685                BuildProfile::Enabled(name) => {
2686                    builder.token(IDENT.into(), name.as_str());
2687                }
2688            }
2689        }
2690        builder.token(R_ANGLE.into(), ">");
2691        builder.finish_node();
2692
2693        let node_profiles = self.0.children().find(|n| n.kind() == PROFILES);
2694        if let Some(node_profiles) = node_profiles {
2695            let new_root = SyntaxNode::new_root_mut(builder.finish());
2696            self.0.splice_children(
2697                node_profiles.index()..node_profiles.index() + 1,
2698                vec![new_root.into()],
2699            );
2700        } else {
2701            let idx = self.0.children_with_tokens().count();
2702            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2703                idx..idx,
2704                vec![
2705                    GreenToken::new(WHITESPACE.into(), " ").into(),
2706                    builder.finish().into(),
2707                ],
2708            ));
2709            if let Some(parent) = self.0.parent() {
2710                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2711                self.0 = parent
2712                    .children_with_tokens()
2713                    .nth(self.0.index())
2714                    .unwrap()
2715                    .clone()
2716                    .into_node()
2717                    .unwrap();
2718            } else {
2719                self.0 = new_root;
2720            }
2721        }
2722    }
2723
2724    /// Build a new relation
2725    pub fn build(name: &str) -> RelationBuilder {
2726        RelationBuilder::new(name)
2727    }
2728
2729    /// Check if this relation is implied by another relation.
2730    ///
2731    /// A relation is implied by another if the outer relation is more restrictive
2732    /// or equal to this relation. For example:
2733    /// - `pkg >= 1.0` is implied by `pkg >= 1.5` (outer is more restrictive)
2734    /// - `pkg >= 1.0` is implied by `pkg = 1.5` (outer is more restrictive)
2735    /// - `pkg` (no version) is implied by any versioned constraint on `pkg`
2736    ///
2737    /// # Arguments
2738    /// * `outer` - The outer relation that may imply this relation
2739    ///
2740    /// # Returns
2741    /// `true` if this relation is implied by `outer`, `false` otherwise
2742    ///
2743    /// # Example
2744    /// ```
2745    /// use debian_control::lossless::relations::Relation;
2746    /// use debian_control::relations::VersionConstraint;
2747    ///
2748    /// let inner = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())));
2749    /// let outer = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())));
2750    /// assert!(inner.is_implied_by(&outer));
2751    ///
2752    /// let inner2 = Relation::new("pkg", None);
2753    /// assert!(inner2.is_implied_by(&outer));
2754    /// ```
2755    pub fn is_implied_by(&self, outer: &Relation) -> bool {
2756        if self.name() != outer.name() {
2757            return false;
2758        }
2759
2760        let inner_version = self.version();
2761        let outer_version = outer.version();
2762
2763        // No version constraint on inner means it's always implied
2764        if inner_version.is_none() {
2765            return true;
2766        }
2767
2768        // If versions are identical, they imply each other
2769        if inner_version == outer_version {
2770            return true;
2771        }
2772
2773        // Inner has version but outer doesn't - not implied
2774        if outer_version.is_none() {
2775            return false;
2776        }
2777
2778        let (inner_constraint, inner_ver) = inner_version.unwrap();
2779        let (outer_constraint, outer_ver) = outer_version.unwrap();
2780
2781        use VersionConstraint::*;
2782        match inner_constraint {
2783            GreaterThanEqual => match outer_constraint {
2784                GreaterThan => outer_ver > inner_ver,
2785                GreaterThanEqual | Equal => outer_ver >= inner_ver,
2786                LessThan | LessThanEqual => false,
2787            },
2788            Equal => match outer_constraint {
2789                Equal => outer_ver == inner_ver,
2790                _ => false,
2791            },
2792            LessThan => match outer_constraint {
2793                LessThan => outer_ver <= inner_ver,
2794                LessThanEqual | Equal => outer_ver < inner_ver,
2795                GreaterThan | GreaterThanEqual => false,
2796            },
2797            LessThanEqual => match outer_constraint {
2798                LessThanEqual | Equal | LessThan => outer_ver <= inner_ver,
2799                GreaterThan | GreaterThanEqual => false,
2800            },
2801            GreaterThan => match outer_constraint {
2802                GreaterThan => outer_ver >= inner_ver,
2803                Equal | GreaterThanEqual => outer_ver > inner_ver,
2804                LessThan | LessThanEqual => false,
2805            },
2806        }
2807    }
2808}
2809
2810/// A builder for creating a `Relation`
2811///
2812/// # Example
2813/// ```
2814/// use debian_control::lossless::relations::{Relation};
2815/// use debian_control::relations::VersionConstraint;
2816/// let relation = Relation::build("samba")
2817///    .version_constraint(VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())
2818///    .archqual("any")
2819///    .architectures(vec!["amd64".to_string(), "i386".to_string()])
2820///    .build();
2821/// assert_eq!(relation.to_string(), "samba:any (>= 2.0) [amd64 i386]");
2822/// ```
2823pub struct RelationBuilder {
2824    name: String,
2825    version_constraint: Option<(VersionConstraint, Version)>,
2826    archqual: Option<String>,
2827    architectures: Option<Vec<String>>,
2828    profiles: Vec<Vec<BuildProfile>>,
2829}
2830
2831impl RelationBuilder {
2832    /// Create a new `RelationBuilder` with the given package name
2833    fn new(name: &str) -> Self {
2834        Self {
2835            name: name.to_string(),
2836            version_constraint: None,
2837            archqual: None,
2838            architectures: None,
2839            profiles: vec![],
2840        }
2841    }
2842
2843    /// Set the version constraint for this relation
2844    pub fn version_constraint(mut self, vc: VersionConstraint, version: Version) -> Self {
2845        self.version_constraint = Some((vc, version));
2846        self
2847    }
2848
2849    /// Set the architecture qualifier for this relation
2850    pub fn archqual(mut self, archqual: &str) -> Self {
2851        self.archqual = Some(archqual.to_string());
2852        self
2853    }
2854
2855    /// Set the architectures for this relation
2856    pub fn architectures(mut self, architectures: Vec<String>) -> Self {
2857        self.architectures = Some(architectures);
2858        self
2859    }
2860
2861    /// Set the build profiles for this relation
2862    pub fn profiles(mut self, profiles: Vec<Vec<BuildProfile>>) -> Self {
2863        self.profiles = profiles;
2864        self
2865    }
2866
2867    /// Add a build profile to this relation
2868    pub fn add_profile(mut self, profile: Vec<BuildProfile>) -> Self {
2869        self.profiles.push(profile);
2870        self
2871    }
2872
2873    /// Build the `Relation`
2874    pub fn build(self) -> Relation {
2875        let mut relation = Relation::new(&self.name, self.version_constraint);
2876        if let Some(archqual) = &self.archqual {
2877            relation.set_archqual(archqual);
2878        }
2879        if let Some(architectures) = &self.architectures {
2880            relation.set_architectures(architectures.iter().map(|s| s.as_str()));
2881        }
2882        for profile in &self.profiles {
2883            relation.add_profile(profile);
2884        }
2885        relation
2886    }
2887}
2888
2889impl PartialOrd for Relation {
2890    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2891        // Compare by name first, then by version
2892        let name_cmp = self.name().cmp(&other.name());
2893        if name_cmp != std::cmp::Ordering::Equal {
2894            return Some(name_cmp);
2895        }
2896
2897        let self_version = self.version();
2898        let other_version = other.version();
2899
2900        match (self_version, other_version) {
2901            (Some((self_vc, self_version)), Some((other_vc, other_version))) => {
2902                let vc_cmp = self_vc.cmp(&other_vc);
2903                if vc_cmp != std::cmp::Ordering::Equal {
2904                    return Some(vc_cmp);
2905                }
2906
2907                Some(self_version.cmp(&other_version))
2908            }
2909            (Some(_), None) => Some(std::cmp::Ordering::Greater),
2910            (None, Some(_)) => Some(std::cmp::Ordering::Less),
2911            (None, None) => Some(std::cmp::Ordering::Equal),
2912        }
2913    }
2914}
2915
2916impl Eq for Relation {}
2917
2918impl Ord for Relation {
2919    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2920        self.partial_cmp(other).unwrap()
2921    }
2922}
2923
2924impl std::str::FromStr for Relations {
2925    type Err = String;
2926
2927    fn from_str(s: &str) -> Result<Self, Self::Err> {
2928        let parse = parse(s, false);
2929        if parse.errors.is_empty() {
2930            Ok(parse.root_mut())
2931        } else {
2932            Err(parse.errors.join("\n"))
2933        }
2934    }
2935}
2936
2937impl std::str::FromStr for Entry {
2938    type Err = String;
2939
2940    fn from_str(s: &str) -> Result<Self, Self::Err> {
2941        let root: Relations = s.parse()?;
2942
2943        let mut entries = root.entries();
2944        let entry = if let Some(entry) = entries.next() {
2945            entry
2946        } else {
2947            return Err("No entry found".to_string());
2948        };
2949
2950        if entries.next().is_some() {
2951            return Err("Multiple entries found".to_string());
2952        }
2953
2954        Ok(entry)
2955    }
2956}
2957
2958impl std::str::FromStr for Relation {
2959    type Err = String;
2960
2961    fn from_str(s: &str) -> Result<Self, Self::Err> {
2962        let entry: Entry = s.parse()?;
2963
2964        let mut relations = entry.relations();
2965        let relation = if let Some(relation) = relations.next() {
2966            relation
2967        } else {
2968            return Err("No relation found".to_string());
2969        };
2970
2971        if relations.next().is_some() {
2972            return Err("Multiple relations found".to_string());
2973        }
2974
2975        Ok(relation)
2976    }
2977}
2978
2979impl From<crate::lossy::Relation> for Relation {
2980    fn from(relation: crate::lossy::Relation) -> Self {
2981        let mut builder = Relation::build(&relation.name);
2982
2983        if let Some((vc, version)) = relation.version {
2984            builder = builder.version_constraint(vc, version);
2985        }
2986
2987        if let Some(archqual) = relation.archqual {
2988            builder = builder.archqual(&archqual);
2989        }
2990
2991        if let Some(architectures) = relation.architectures {
2992            builder = builder.architectures(architectures);
2993        }
2994
2995        builder = builder.profiles(relation.profiles);
2996
2997        builder.build()
2998    }
2999}
3000
3001impl From<Relation> for crate::lossy::Relation {
3002    fn from(relation: Relation) -> Self {
3003        crate::lossy::Relation {
3004            name: relation.name(),
3005            version: relation.version(),
3006            archqual: relation.archqual(),
3007            architectures: relation.architectures().map(|a| a.collect()),
3008            profiles: relation.profiles().collect(),
3009        }
3010    }
3011}
3012
3013impl From<Entry> for Vec<crate::lossy::Relation> {
3014    fn from(entry: Entry) -> Self {
3015        entry.relations().map(|r| r.into()).collect()
3016    }
3017}
3018
3019impl From<Vec<crate::lossy::Relation>> for Entry {
3020    fn from(relations: Vec<crate::lossy::Relation>) -> Self {
3021        let relations: Vec<Relation> = relations.into_iter().map(|r| r.into()).collect();
3022        Entry::from(relations)
3023    }
3024}
3025
3026#[cfg(test)]
3027mod tests {
3028    use super::*;
3029
3030    #[test]
3031    fn test_parse() {
3032        let input = "python3-dulwich";
3033        let parsed: Relations = input.parse().unwrap();
3034        assert_eq!(parsed.to_string(), input);
3035        assert_eq!(parsed.entries().count(), 1);
3036        let entry = parsed.entries().next().unwrap();
3037        assert_eq!(entry.to_string(), "python3-dulwich");
3038        assert_eq!(entry.relations().count(), 1);
3039        let relation = entry.relations().next().unwrap();
3040        assert_eq!(relation.to_string(), "python3-dulwich");
3041        assert_eq!(relation.version(), None);
3042
3043        let input = "python3-dulwich (>= 0.20.21)";
3044        let parsed: Relations = input.parse().unwrap();
3045        assert_eq!(parsed.to_string(), input);
3046        assert_eq!(parsed.entries().count(), 1);
3047        let entry = parsed.entries().next().unwrap();
3048        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3049        assert_eq!(entry.relations().count(), 1);
3050        let relation = entry.relations().next().unwrap();
3051        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3052        assert_eq!(
3053            relation.version(),
3054            Some((
3055                VersionConstraint::GreaterThanEqual,
3056                "0.20.21".parse().unwrap()
3057            ))
3058        );
3059    }
3060
3061    #[test]
3062    fn test_multiple() {
3063        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
3064        let parsed: Relations = input.parse().unwrap();
3065        assert_eq!(parsed.to_string(), input);
3066        assert_eq!(parsed.entries().count(), 2);
3067        let entry = parsed.entries().next().unwrap();
3068        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3069        assert_eq!(entry.relations().count(), 1);
3070        let relation = entry.relations().next().unwrap();
3071        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3072        assert_eq!(
3073            relation.version(),
3074            Some((
3075                VersionConstraint::GreaterThanEqual,
3076                "0.20.21".parse().unwrap()
3077            ))
3078        );
3079        let entry = parsed.entries().nth(1).unwrap();
3080        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.21)");
3081        assert_eq!(entry.relations().count(), 1);
3082        let relation = entry.relations().next().unwrap();
3083        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
3084        assert_eq!(
3085            relation.version(),
3086            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
3087        );
3088    }
3089
3090    #[test]
3091    fn test_architectures() {
3092        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
3093        let parsed: Relations = input.parse().unwrap();
3094        assert_eq!(parsed.to_string(), input);
3095        assert_eq!(parsed.entries().count(), 1);
3096        let entry = parsed.entries().next().unwrap();
3097        assert_eq!(
3098            entry.to_string(),
3099            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3100        );
3101        assert_eq!(entry.relations().count(), 1);
3102        let relation = entry.relations().next().unwrap();
3103        assert_eq!(
3104            relation.to_string(),
3105            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3106        );
3107        assert_eq!(relation.version(), None);
3108        assert_eq!(
3109            relation.architectures().unwrap().collect::<Vec<_>>(),
3110            vec![
3111                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
3112            ]
3113            .into_iter()
3114            .map(|s| s.to_string())
3115            .collect::<Vec<_>>()
3116        );
3117    }
3118
3119    #[test]
3120    fn test_profiles() {
3121        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
3122        let parsed: Relations = input.parse().unwrap();
3123        assert_eq!(parsed.to_string(), input);
3124        assert_eq!(parsed.entries().count(), 2);
3125        let entry = parsed.entries().next().unwrap();
3126        assert_eq!(
3127            entry.to_string(),
3128            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3129        );
3130        assert_eq!(entry.relations().count(), 1);
3131        let relation = entry.relations().next().unwrap();
3132        assert_eq!(
3133            relation.to_string(),
3134            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3135        );
3136        assert_eq!(
3137            relation.version(),
3138            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
3139        );
3140        assert_eq!(
3141            relation.architectures().unwrap().collect::<Vec<_>>(),
3142            vec!["i386", "arm"]
3143                .into_iter()
3144                .map(|s| s.to_string())
3145                .collect::<Vec<_>>()
3146        );
3147        assert_eq!(
3148            relation.profiles().collect::<Vec<_>>(),
3149            vec![
3150                vec![BuildProfile::Disabled("nocheck".to_string())],
3151                vec![BuildProfile::Disabled("cross".to_string())]
3152            ]
3153        );
3154    }
3155
3156    #[test]
3157    fn test_substvar() {
3158        let input = "${shlibs:Depends}";
3159
3160        let (parsed, errors) = Relations::parse_relaxed(input, true);
3161        assert_eq!(errors, Vec::<String>::new());
3162        assert_eq!(parsed.to_string(), input);
3163        assert_eq!(parsed.entries().count(), 0);
3164
3165        assert_eq!(
3166            parsed.substvars().collect::<Vec<_>>(),
3167            vec!["${shlibs:Depends}"]
3168        );
3169    }
3170
3171    #[test]
3172    fn test_new() {
3173        let r = Relation::new(
3174            "samba",
3175            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3176        );
3177
3178        assert_eq!(r.to_string(), "samba (>= 2.0)");
3179    }
3180
3181    #[test]
3182    fn test_drop_constraint() {
3183        let mut r = Relation::new(
3184            "samba",
3185            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3186        );
3187
3188        r.drop_constraint();
3189
3190        assert_eq!(r.to_string(), "samba");
3191    }
3192
3193    #[test]
3194    fn test_simple() {
3195        let r = Relation::simple("samba");
3196
3197        assert_eq!(r.to_string(), "samba");
3198    }
3199
3200    #[test]
3201    fn test_remove_first_entry() {
3202        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3203            .parse()
3204            .unwrap();
3205        let removed = rels.remove_entry(0);
3206        assert_eq!(removed.to_string(), "python3-dulwich (>= 0.20.21)");
3207        assert_eq!(rels.to_string(), "python3-dulwich (<< 0.21)");
3208    }
3209
3210    #[test]
3211    fn test_remove_last_entry() {
3212        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3213            .parse()
3214            .unwrap();
3215        rels.remove_entry(1);
3216        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3217    }
3218
3219    #[test]
3220    fn test_remove_middle() {
3221        let mut rels: Relations =
3222            r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21), python3-dulwich (<< 0.22)"#
3223                .parse()
3224                .unwrap();
3225        rels.remove_entry(1);
3226        assert_eq!(
3227            rels.to_string(),
3228            "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.22)"
3229        );
3230    }
3231
3232    #[test]
3233    fn test_remove_added() {
3234        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3235        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3236        rels.push(entry);
3237        rels.remove_entry(1);
3238        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3239    }
3240
3241    #[test]
3242    fn test_push() {
3243        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3244        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3245        rels.push(entry);
3246        assert_eq!(
3247            rels.to_string(),
3248            "python3-dulwich (>= 0.20.21), python3-dulwich"
3249        );
3250    }
3251
3252    #[test]
3253    fn test_insert_with_custom_separator() {
3254        let mut rels: Relations = "python3".parse().unwrap();
3255        let entry = Entry::from(vec![Relation::simple("debhelper")]);
3256        rels.insert_with_separator(1, entry, Some("\n "));
3257        assert_eq!(rels.to_string(), "python3,\n debhelper");
3258    }
3259
3260    #[test]
3261    fn test_insert_with_wrap_and_sort_separator() {
3262        let mut rels: Relations = "python3".parse().unwrap();
3263        let entry = Entry::from(vec![Relation::simple("rustc")]);
3264        // Simulate wrap-and-sort -a style with field name "Depends: " (9 chars)
3265        rels.insert_with_separator(1, entry, Some("\n         "));
3266        assert_eq!(rels.to_string(), "python3,\n         rustc");
3267    }
3268
3269    #[test]
3270    fn test_push_from_empty() {
3271        let mut rels: Relations = "".parse().unwrap();
3272        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3273        rels.push(entry);
3274        assert_eq!(rels.to_string(), "python3-dulwich");
3275    }
3276
3277    #[test]
3278    fn test_insert() {
3279        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3280            .parse()
3281            .unwrap();
3282        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3283        rels.insert(1, entry);
3284        assert_eq!(
3285            rels.to_string(),
3286            "python3-dulwich (>= 0.20.21), python3-dulwich, python3-dulwich (<< 0.21)"
3287        );
3288    }
3289
3290    #[test]
3291    fn test_insert_at_start() {
3292        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3293            .parse()
3294            .unwrap();
3295        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3296        rels.insert(0, entry);
3297        assert_eq!(
3298            rels.to_string(),
3299            "python3-dulwich, python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3300        );
3301    }
3302
3303    #[test]
3304    fn test_insert_after_error() {
3305        let (mut rels, errors) = Relations::parse_relaxed("@foo@, debhelper (>= 1.0)", false);
3306        assert_eq!(
3307            errors,
3308            vec![
3309                "expected $ or identifier but got ERROR",
3310                "expected comma or end of file but got Some(IDENT)",
3311                "expected $ or identifier but got ERROR"
3312            ]
3313        );
3314        let entry = Entry::from(vec![Relation::simple("bar")]);
3315        rels.push(entry);
3316        assert_eq!(rels.to_string(), "@foo@, debhelper (>= 1.0), bar");
3317    }
3318
3319    #[test]
3320    fn test_insert_before_error() {
3321        let (mut rels, errors) = Relations::parse_relaxed("debhelper (>= 1.0), @foo@, bla", false);
3322        assert_eq!(
3323            errors,
3324            vec![
3325                "expected $ or identifier but got ERROR",
3326                "expected comma or end of file but got Some(IDENT)",
3327                "expected $ or identifier but got ERROR"
3328            ]
3329        );
3330        let entry = Entry::from(vec![Relation::simple("bar")]);
3331        rels.insert(0, entry);
3332        assert_eq!(rels.to_string(), "bar, debhelper (>= 1.0), @foo@, bla");
3333    }
3334
3335    #[test]
3336    fn test_replace() {
3337        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3338            .parse()
3339            .unwrap();
3340        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3341        rels.replace(1, entry);
3342        assert_eq!(
3343            rels.to_string(),
3344            "python3-dulwich (>= 0.20.21), python3-dulwich"
3345        );
3346    }
3347
3348    #[test]
3349    fn test_relation_from_entries() {
3350        let entries = vec![
3351            Entry::from(vec![Relation::simple("python3-dulwich")]),
3352            Entry::from(vec![Relation::simple("python3-breezy")]),
3353        ];
3354        let rels: Relations = entries.into();
3355        assert_eq!(rels.entries().count(), 2);
3356        assert_eq!(rels.to_string(), "python3-dulwich, python3-breezy");
3357    }
3358
3359    #[test]
3360    fn test_entry_from_relations() {
3361        let relations = vec![
3362            Relation::simple("python3-dulwich"),
3363            Relation::simple("python3-breezy"),
3364        ];
3365        let entry: Entry = relations.into();
3366        assert_eq!(entry.relations().count(), 2);
3367        assert_eq!(entry.to_string(), "python3-dulwich | python3-breezy");
3368    }
3369
3370    #[test]
3371    fn test_parse_entry() {
3372        let parsed: Entry = "python3-dulwich (>= 0.20.21) | bar".parse().unwrap();
3373        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21) | bar");
3374        assert_eq!(parsed.relations().count(), 2);
3375
3376        assert_eq!(
3377            "foo, bar".parse::<Entry>().unwrap_err(),
3378            "Multiple entries found"
3379        );
3380        assert_eq!("".parse::<Entry>().unwrap_err(), "No entry found");
3381    }
3382
3383    #[test]
3384    fn test_parse_relation() {
3385        let parsed: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3386        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
3387        assert_eq!(
3388            parsed.version(),
3389            Some((
3390                VersionConstraint::GreaterThanEqual,
3391                "0.20.21".parse().unwrap()
3392            ))
3393        );
3394        assert_eq!(
3395            "foo | bar".parse::<Relation>().unwrap_err(),
3396            "Multiple relations found"
3397        );
3398        assert_eq!("".parse::<Relation>().unwrap_err(), "No entry found");
3399    }
3400
3401    #[test]
3402    fn test_special() {
3403        let parsed: Relation = "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3404            .parse()
3405            .unwrap();
3406        assert_eq!(
3407            parsed.to_string(),
3408            "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3409        );
3410        assert_eq!(
3411            parsed.version(),
3412            Some((
3413                VersionConstraint::GreaterThanEqual,
3414                "0.1.138-~~".parse().unwrap()
3415            ))
3416        );
3417        assert_eq!(parsed.archqual(), Some("amd64".to_string()));
3418        assert_eq!(parsed.name(), "librust-breezyshim+dirty-tracker-dev");
3419    }
3420
3421    #[test]
3422    fn test_relations_satisfied_by() {
3423        let rels: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3424            .parse()
3425            .unwrap();
3426        let satisfied = |name: &str| -> Option<debversion::Version> {
3427            match name {
3428                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3429                _ => None,
3430            }
3431        };
3432        assert!(rels.satisfied_by(satisfied));
3433
3434        let satisfied = |name: &str| match name {
3435            "python3-dulwich" => Some("0.21".parse().unwrap()),
3436            _ => None,
3437        };
3438        assert!(!rels.satisfied_by(satisfied));
3439
3440        let satisfied = |name: &str| match name {
3441            "python3-dulwich" => Some("0.20.20".parse().unwrap()),
3442            _ => None,
3443        };
3444        assert!(!rels.satisfied_by(satisfied));
3445    }
3446
3447    #[test]
3448    fn test_entry_satisfied_by() {
3449        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3450            .parse()
3451            .unwrap();
3452        let satisfied = |name: &str| -> Option<debversion::Version> {
3453            match name {
3454                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3455                _ => None,
3456            }
3457        };
3458        assert!(entry.satisfied_by(satisfied));
3459        let satisfied = |name: &str| -> Option<debversion::Version> {
3460            match name {
3461                "python3-dulwich" => Some("0.18".parse().unwrap()),
3462                _ => None,
3463            }
3464        };
3465        assert!(!entry.satisfied_by(satisfied));
3466    }
3467
3468    #[test]
3469    fn test_wrap_and_sort_relation() {
3470        let relation: Relation = "   python3-dulwich   (>= 11) [  amd64 ] <  lala>"
3471            .parse()
3472            .unwrap();
3473
3474        let wrapped = relation.wrap_and_sort();
3475
3476        assert_eq!(
3477            wrapped.to_string(),
3478            "python3-dulwich (>= 11) [amd64] <lala>"
3479        );
3480    }
3481
3482    #[test]
3483    fn test_wrap_and_sort_relations() {
3484        let entry: Relations =
3485            "python3-dulwich (>= 0.20.21)   | bar, \n\n\n\npython3-dulwich (<< 0.21)"
3486                .parse()
3487                .unwrap();
3488
3489        let wrapped = entry.wrap_and_sort();
3490
3491        assert_eq!(
3492            wrapped.to_string(),
3493            "bar | python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3494        );
3495    }
3496
3497    #[cfg(feature = "serde")]
3498    #[test]
3499    fn test_serialize_relations() {
3500        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3501            .parse()
3502            .unwrap();
3503        let serialized = serde_json::to_string(&relations).unwrap();
3504        assert_eq!(
3505            serialized,
3506            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
3507        );
3508    }
3509
3510    #[cfg(feature = "serde")]
3511    #[test]
3512    fn test_deserialize_relations() {
3513        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3514            .parse()
3515            .unwrap();
3516        let serialized = serde_json::to_string(&relations).unwrap();
3517        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
3518        assert_eq!(deserialized.to_string(), relations.to_string());
3519    }
3520
3521    #[cfg(feature = "serde")]
3522    #[test]
3523    fn test_serialize_relation() {
3524        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3525        let serialized = serde_json::to_string(&relation).unwrap();
3526        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
3527    }
3528
3529    #[cfg(feature = "serde")]
3530    #[test]
3531    fn test_deserialize_relation() {
3532        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3533        let serialized = serde_json::to_string(&relation).unwrap();
3534        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
3535        assert_eq!(deserialized.to_string(), relation.to_string());
3536    }
3537
3538    #[cfg(feature = "serde")]
3539    #[test]
3540    fn test_serialize_entry() {
3541        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3542            .parse()
3543            .unwrap();
3544        let serialized = serde_json::to_string(&entry).unwrap();
3545        assert_eq!(
3546            serialized,
3547            r#""python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)""#
3548        );
3549    }
3550
3551    #[cfg(feature = "serde")]
3552    #[test]
3553    fn test_deserialize_entry() {
3554        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3555            .parse()
3556            .unwrap();
3557        let serialized = serde_json::to_string(&entry).unwrap();
3558        let deserialized: Entry = serde_json::from_str(&serialized).unwrap();
3559        assert_eq!(deserialized.to_string(), entry.to_string());
3560    }
3561
3562    #[test]
3563    fn test_remove_first_relation() {
3564        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3565            .parse()
3566            .unwrap();
3567        let mut rel = entry.relations().next().unwrap();
3568        rel.remove();
3569        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.18)");
3570    }
3571
3572    #[test]
3573    fn test_remove_last_relation() {
3574        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3575            .parse()
3576            .unwrap();
3577        let mut rel = entry.relations().nth(1).unwrap();
3578        rel.remove();
3579        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3580    }
3581
3582    #[test]
3583    fn test_remove_only_relation() {
3584        let entry: Entry = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3585        let mut rel = entry.relations().next().unwrap();
3586        rel.remove();
3587        assert_eq!(entry.to_string(), "");
3588    }
3589
3590    #[test]
3591    fn test_relations_is_empty() {
3592        let entry: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3593        assert!(!entry.is_empty());
3594        assert_eq!(1, entry.len());
3595        let mut rel = entry.entries().next().unwrap();
3596        rel.remove();
3597        assert!(entry.is_empty());
3598        assert_eq!(0, entry.len());
3599    }
3600
3601    #[test]
3602    fn test_entry_is_empty() {
3603        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3604            .parse()
3605            .unwrap();
3606        assert!(!entry.is_empty());
3607        assert_eq!(2, entry.len());
3608        let mut rel = entry.relations().next().unwrap();
3609        rel.remove();
3610        assert!(!entry.is_empty());
3611        assert_eq!(1, entry.len());
3612        let mut rel = entry.relations().next().unwrap();
3613        rel.remove();
3614        assert!(entry.is_empty());
3615        assert_eq!(0, entry.len());
3616    }
3617
3618    #[test]
3619    fn test_relation_set_version() {
3620        let mut rel: Relation = "samba".parse().unwrap();
3621        rel.set_version(None);
3622        assert_eq!("samba", rel.to_string());
3623        rel.set_version(Some((
3624            VersionConstraint::GreaterThanEqual,
3625            "2.0".parse().unwrap(),
3626        )));
3627        assert_eq!("samba (>= 2.0)", rel.to_string());
3628        rel.set_version(None);
3629        assert_eq!("samba", rel.to_string());
3630        rel.set_version(Some((
3631            VersionConstraint::GreaterThanEqual,
3632            "2.0".parse().unwrap(),
3633        )));
3634        rel.set_version(Some((
3635            VersionConstraint::GreaterThanEqual,
3636            "1.1".parse().unwrap(),
3637        )));
3638        assert_eq!("samba (>= 1.1)", rel.to_string());
3639    }
3640
3641    #[test]
3642    fn test_replace_relation() {
3643        let mut entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3644            .parse()
3645            .unwrap();
3646        let new_rel = Relation::simple("python3-breezy");
3647        entry.replace(0, new_rel);
3648        assert_eq!(
3649            entry.to_string(),
3650            "python3-breezy | python3-dulwich (<< 0.18)"
3651        );
3652    }
3653
3654    #[test]
3655    fn test_entry_push_relation() {
3656        let relations: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3657        let new_rel = Relation::simple("python3-breezy");
3658        let mut entry = relations.entries().next().unwrap();
3659        entry.push(new_rel);
3660        assert_eq!(
3661            entry.to_string(),
3662            "python3-dulwich (>= 0.20.21) | python3-breezy"
3663        );
3664        assert_eq!(
3665            relations.to_string(),
3666            "python3-dulwich (>= 0.20.21) | python3-breezy"
3667        );
3668    }
3669
3670    #[test]
3671    fn test_relations_remove_empty_entry() {
3672        let (mut relations, errors) = Relations::parse_relaxed("foo, , bar, ", false);
3673        assert_eq!(errors, Vec::<String>::new());
3674        assert_eq!(relations.to_string(), "foo, , bar, ");
3675        assert_eq!(relations.len(), 2);
3676        assert_eq!(
3677            relations.entries().next().unwrap().to_string(),
3678            "foo".to_string()
3679        );
3680        assert_eq!(
3681            relations.entries().nth(1).unwrap().to_string(),
3682            "bar".to_string()
3683        );
3684        relations.remove_entry(1);
3685        assert_eq!(relations.to_string(), "foo, , ");
3686    }
3687
3688    #[test]
3689    fn test_entry_remove_relation() {
3690        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3691        let removed = entry.remove_relation(0);
3692        assert_eq!(removed.to_string(), "python3-dulwich");
3693        assert_eq!(entry.to_string(), "samba");
3694    }
3695
3696    #[test]
3697    fn test_wrap_and_sort_removes_empty_entries() {
3698        let relations: Relations = "foo, , bar, ".parse().unwrap();
3699        let wrapped = relations.wrap_and_sort();
3700        assert_eq!(wrapped.to_string(), "bar, foo");
3701    }
3702
3703    #[test]
3704    fn test_set_archqual() {
3705        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3706        let mut rel = entry.relations().next().unwrap();
3707        rel.set_archqual("amd64");
3708        assert_eq!(rel.to_string(), "python3-dulwich:amd64");
3709        assert_eq!(rel.archqual(), Some("amd64".to_string()));
3710        assert_eq!(entry.to_string(), "python3-dulwich:amd64 | samba");
3711        rel.set_archqual("i386");
3712        assert_eq!(rel.to_string(), "python3-dulwich:i386");
3713        assert_eq!(rel.archqual(), Some("i386".to_string()));
3714        assert_eq!(entry.to_string(), "python3-dulwich:i386 | samba");
3715    }
3716
3717    #[test]
3718    fn test_set_architectures() {
3719        let mut relation = Relation::simple("samba");
3720        relation.set_architectures(vec!["amd64", "i386"].into_iter());
3721        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3722    }
3723
3724    #[test]
3725    fn test_relation_builder_no_architectures() {
3726        // Test that building a relation without architectures doesn't add empty brackets
3727        let relation = Relation::build("debhelper").build();
3728        assert_eq!(relation.to_string(), "debhelper");
3729    }
3730
3731    #[test]
3732    fn test_relation_builder_with_architectures() {
3733        // Test that building a relation with architectures works correctly
3734        let relation = Relation::build("samba")
3735            .architectures(vec!["amd64".to_string(), "i386".to_string()])
3736            .build();
3737        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3738    }
3739
3740    #[test]
3741    fn test_ensure_minimum_version_add_new() {
3742        let mut relations: Relations = "python3".parse().unwrap();
3743        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3744        assert_eq!(relations.to_string(), "python3, debhelper (>= 12)");
3745    }
3746
3747    #[test]
3748    fn test_ensure_minimum_version_update() {
3749        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3750        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3751        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3752    }
3753
3754    #[test]
3755    fn test_ensure_minimum_version_no_change() {
3756        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
3757        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3758        assert_eq!(relations.to_string(), "debhelper (>= 13)");
3759    }
3760
3761    #[test]
3762    fn test_ensure_minimum_version_no_version() {
3763        let mut relations: Relations = "debhelper".parse().unwrap();
3764        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3765        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3766    }
3767
3768    #[test]
3769    fn test_ensure_minimum_version_preserves_newline() {
3770        // Test that newline after the field name is preserved
3771        // This is the format often used in Debian control files:
3772        // Build-Depends:
3773        //  debhelper (>= 9),
3774        //  pkg-config
3775        let input = "\n debhelper (>= 9),\n pkg-config,\n uuid-dev";
3776        let mut relations: Relations = input.parse().unwrap();
3777        relations.ensure_minimum_version("debhelper", &"12~".parse().unwrap());
3778        let result = relations.to_string();
3779
3780        // The newline before the first entry should be preserved
3781        assert!(
3782            result.starts_with('\n'),
3783            "Expected result to start with newline, got: {:?}",
3784            result
3785        );
3786        assert_eq!(result, "\n debhelper (>= 12~),\n pkg-config,\n uuid-dev");
3787    }
3788
3789    #[test]
3790    fn test_ensure_minimum_version_preserves_newline_in_control() {
3791        // Test the full scenario from the bug report
3792        use crate::lossless::Control;
3793        use std::str::FromStr;
3794
3795        let input = r#"Source: f2fs-tools
3796Section: admin
3797Priority: optional
3798Maintainer: Test <test@example.com>
3799Build-Depends:
3800 debhelper (>= 9),
3801 pkg-config,
3802 uuid-dev
3803
3804Package: f2fs-tools
3805Description: test
3806"#;
3807
3808        let control = Control::from_str(input).unwrap();
3809        let mut source = control.source().unwrap();
3810        let mut build_depends = source.build_depends().unwrap();
3811
3812        let version = Version::from_str("12~").unwrap();
3813        build_depends.ensure_minimum_version("debhelper", &version);
3814
3815        source.set_build_depends(&build_depends);
3816
3817        let output = control.to_string();
3818
3819        // Check that the formatting is preserved - the newline after "Build-Depends:" should still be there
3820        assert!(
3821            output.contains("Build-Depends:\n debhelper (>= 12~)"),
3822            "Expected 'Build-Depends:\\n debhelper (>= 12~)' but got:\n{}",
3823            output
3824        );
3825    }
3826
3827    #[test]
3828    fn test_ensure_exact_version_add_new() {
3829        let mut relations: Relations = "python3".parse().unwrap();
3830        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3831        assert_eq!(relations.to_string(), "python3, debhelper (= 12)");
3832    }
3833
3834    #[test]
3835    fn test_ensure_exact_version_update() {
3836        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3837        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3838        assert_eq!(relations.to_string(), "debhelper (= 12)");
3839    }
3840
3841    #[test]
3842    fn test_ensure_exact_version_no_change() {
3843        let mut relations: Relations = "debhelper (= 12)".parse().unwrap();
3844        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3845        assert_eq!(relations.to_string(), "debhelper (= 12)");
3846    }
3847
3848    #[test]
3849    fn test_ensure_some_version_add_new() {
3850        let mut relations: Relations = "python3".parse().unwrap();
3851        relations.ensure_some_version("debhelper");
3852        assert_eq!(relations.to_string(), "python3, debhelper");
3853    }
3854
3855    #[test]
3856    fn test_ensure_some_version_exists_with_version() {
3857        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
3858        relations.ensure_some_version("debhelper");
3859        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3860    }
3861
3862    #[test]
3863    fn test_ensure_some_version_exists_no_version() {
3864        let mut relations: Relations = "debhelper".parse().unwrap();
3865        relations.ensure_some_version("debhelper");
3866        assert_eq!(relations.to_string(), "debhelper");
3867    }
3868
3869    #[test]
3870    fn test_ensure_substvar() {
3871        let mut relations: Relations = "python3".parse().unwrap();
3872        relations.ensure_substvar("${misc:Depends}").unwrap();
3873        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
3874    }
3875
3876    #[test]
3877    fn test_ensure_substvar_already_exists() {
3878        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
3879        relations.ensure_substvar("${misc:Depends}").unwrap();
3880        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
3881    }
3882
3883    #[test]
3884    fn test_ensure_substvar_empty_relations() {
3885        let mut relations: Relations = Relations::new();
3886        relations.ensure_substvar("${misc:Depends}").unwrap();
3887        assert_eq!(relations.to_string(), "${misc:Depends}");
3888    }
3889
3890    #[test]
3891    fn test_ensure_substvar_preserves_whitespace() {
3892        // Test with non-standard whitespace (multiple spaces)
3893        let (mut relations, _) = Relations::parse_relaxed("python3,  rustc", false);
3894        relations.ensure_substvar("${misc:Depends}").unwrap();
3895        // Should preserve the double-space pattern
3896        assert_eq!(relations.to_string(), "python3,  rustc,  ${misc:Depends}");
3897    }
3898
3899    #[test]
3900    fn test_drop_substvar_basic() {
3901        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
3902        relations.drop_substvar("${misc:Depends}");
3903        assert_eq!(relations.to_string(), "python3");
3904    }
3905
3906    #[test]
3907    fn test_drop_substvar_first_position() {
3908        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}, python3", true);
3909        relations.drop_substvar("${misc:Depends}");
3910        assert_eq!(relations.to_string(), "python3");
3911    }
3912
3913    #[test]
3914    fn test_drop_substvar_middle_position() {
3915        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
3916        relations.drop_substvar("${misc:Depends}");
3917        assert_eq!(relations.to_string(), "python3, rustc");
3918    }
3919
3920    #[test]
3921    fn test_drop_substvar_only_substvar() {
3922        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}", true);
3923        relations.drop_substvar("${misc:Depends}");
3924        assert_eq!(relations.to_string(), "");
3925    }
3926
3927    #[test]
3928    fn test_drop_substvar_not_exists() {
3929        let (mut relations, _) = Relations::parse_relaxed("python3, rustc", false);
3930        relations.drop_substvar("${misc:Depends}");
3931        assert_eq!(relations.to_string(), "python3, rustc");
3932    }
3933
3934    #[test]
3935    fn test_drop_substvar_multiple_substvars() {
3936        let (mut relations, _) =
3937            Relations::parse_relaxed("python3, ${misc:Depends}, ${shlibs:Depends}", true);
3938        relations.drop_substvar("${misc:Depends}");
3939        assert_eq!(relations.to_string(), "python3, ${shlibs:Depends}");
3940    }
3941
3942    #[test]
3943    fn test_drop_substvar_preserves_whitespace() {
3944        let (mut relations, _) = Relations::parse_relaxed("python3,  ${misc:Depends}", true);
3945        relations.drop_substvar("${misc:Depends}");
3946        assert_eq!(relations.to_string(), "python3");
3947    }
3948
3949    #[test]
3950    fn test_filter_entries_basic() {
3951        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
3952        relations.filter_entries(|entry| entry.relations().any(|r| r.name().starts_with("python")));
3953        assert_eq!(relations.to_string(), "python3");
3954    }
3955
3956    #[test]
3957    fn test_filter_entries_keep_all() {
3958        let mut relations: Relations = "python3, debhelper".parse().unwrap();
3959        relations.filter_entries(|_| true);
3960        assert_eq!(relations.to_string(), "python3, debhelper");
3961    }
3962
3963    #[test]
3964    fn test_filter_entries_remove_all() {
3965        let mut relations: Relations = "python3, debhelper".parse().unwrap();
3966        relations.filter_entries(|_| false);
3967        assert_eq!(relations.to_string(), "");
3968    }
3969
3970    #[test]
3971    fn test_filter_entries_keep_middle() {
3972        let mut relations: Relations = "aaa, bbb, ccc".parse().unwrap();
3973        relations.filter_entries(|entry| entry.relations().any(|r| r.name() == "bbb"));
3974        assert_eq!(relations.to_string(), "bbb");
3975    }
3976
3977    // Tests for new convenience methods
3978
3979    #[test]
3980    fn test_is_sorted_wrap_and_sort_order() {
3981        // Sorted according to WrapAndSortOrder
3982        let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
3983        assert!(relations.is_sorted(&WrapAndSortOrder));
3984
3985        // Not sorted
3986        let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
3987        assert!(!relations.is_sorted(&WrapAndSortOrder));
3988
3989        // Build systems first (sorted alphabetically within their group)
3990        let (relations, _) =
3991            Relations::parse_relaxed("cdbs, debhelper-compat, python3, ${misc:Depends}", true);
3992        assert!(relations.is_sorted(&WrapAndSortOrder));
3993    }
3994
3995    #[test]
3996    fn test_is_sorted_default_order() {
3997        // Sorted alphabetically
3998        let relations: Relations = "aaa, bbb, ccc".parse().unwrap();
3999        assert!(relations.is_sorted(&DefaultSortingOrder));
4000
4001        // Not sorted
4002        let relations: Relations = "ccc, aaa, bbb".parse().unwrap();
4003        assert!(!relations.is_sorted(&DefaultSortingOrder));
4004
4005        // Special items at end
4006        let (relations, _) = Relations::parse_relaxed("aaa, bbb, ${misc:Depends}", true);
4007        assert!(relations.is_sorted(&DefaultSortingOrder));
4008    }
4009
4010    #[test]
4011    fn test_is_sorted_with_substvars() {
4012        // Substvars should be ignored by DefaultSortingOrder
4013        let (relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
4014        // This is considered sorted because ${misc:Depends} is ignored
4015        assert!(relations.is_sorted(&DefaultSortingOrder));
4016    }
4017
4018    #[test]
4019    fn test_drop_dependency_exists() {
4020        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4021        assert!(relations.drop_dependency("debhelper"));
4022        assert_eq!(relations.to_string(), "python3, rustc");
4023    }
4024
4025    #[test]
4026    fn test_drop_dependency_not_exists() {
4027        let mut relations: Relations = "python3, rustc".parse().unwrap();
4028        assert!(!relations.drop_dependency("nonexistent"));
4029        assert_eq!(relations.to_string(), "python3, rustc");
4030    }
4031
4032    #[test]
4033    fn test_drop_dependency_only_item() {
4034        let mut relations: Relations = "python3".parse().unwrap();
4035        assert!(relations.drop_dependency("python3"));
4036        assert_eq!(relations.to_string(), "");
4037    }
4038
4039    #[test]
4040    fn test_add_dependency_to_empty() {
4041        let mut relations: Relations = "".parse().unwrap();
4042        let entry = Entry::from(Relation::simple("python3"));
4043        relations.add_dependency(entry, None);
4044        assert_eq!(relations.to_string(), "python3");
4045    }
4046
4047    #[test]
4048    fn test_add_dependency_sorted_position() {
4049        let mut relations: Relations = "debhelper, rustc".parse().unwrap();
4050        let entry = Entry::from(Relation::simple("python3"));
4051        relations.add_dependency(entry, None);
4052        // Should be inserted in sorted position
4053        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4054    }
4055
4056    #[test]
4057    fn test_add_dependency_explicit_position() {
4058        let mut relations: Relations = "python3, rustc".parse().unwrap();
4059        let entry = Entry::from(Relation::simple("debhelper"));
4060        relations.add_dependency(entry, Some(0));
4061        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4062    }
4063
4064    #[test]
4065    fn test_add_dependency_build_system_first() {
4066        let mut relations: Relations = "python3, rustc".parse().unwrap();
4067        let entry = Entry::from(Relation::simple("debhelper-compat"));
4068        relations.add_dependency(entry, None);
4069        // debhelper-compat should be inserted first (build system)
4070        assert_eq!(relations.to_string(), "debhelper-compat, python3, rustc");
4071    }
4072
4073    #[test]
4074    fn test_add_dependency_at_end() {
4075        let mut relations: Relations = "debhelper, python3".parse().unwrap();
4076        let entry = Entry::from(Relation::simple("zzz-package"));
4077        relations.add_dependency(entry, None);
4078        // Should be added at the end (alphabetically after python3)
4079        assert_eq!(relations.to_string(), "debhelper, python3, zzz-package");
4080    }
4081
4082    #[test]
4083    fn test_add_dependency_to_single_entry() {
4084        // Regression test: ensure comma is added when inserting into single-entry Relations
4085        let mut relations: Relations = "python3-dulwich".parse().unwrap();
4086        let entry: Entry = "debhelper-compat (= 12)".parse().unwrap();
4087        relations.add_dependency(entry, None);
4088        // Should insert with comma separator
4089        assert_eq!(
4090            relations.to_string(),
4091            "debhelper-compat (= 12), python3-dulwich"
4092        );
4093    }
4094
4095    #[test]
4096    fn test_get_relation_exists() {
4097        let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
4098        let result = relations.get_relation("debhelper");
4099        assert!(result.is_ok());
4100        let (idx, entry) = result.unwrap();
4101        assert_eq!(idx, 1);
4102        assert_eq!(entry.to_string(), "debhelper (>= 12)");
4103    }
4104
4105    #[test]
4106    fn test_get_relation_not_exists() {
4107        let relations: Relations = "python3, rustc".parse().unwrap();
4108        let result = relations.get_relation("nonexistent");
4109        assert_eq!(result, Err("Package nonexistent not found".to_string()));
4110    }
4111
4112    #[test]
4113    fn test_get_relation_complex_rule() {
4114        let relations: Relations = "python3 | python3-minimal, rustc".parse().unwrap();
4115        let result = relations.get_relation("python3");
4116        assert_eq!(
4117            result,
4118            Err("Complex rule for python3, aborting".to_string())
4119        );
4120    }
4121
4122    #[test]
4123    fn test_iter_relations_for_simple() {
4124        let relations: Relations = "python3, debhelper, python3-dev".parse().unwrap();
4125        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4126        assert_eq!(entries.len(), 1);
4127        assert_eq!(entries[0].0, 0);
4128        assert_eq!(entries[0].1.to_string(), "python3");
4129    }
4130
4131    #[test]
4132    fn test_iter_relations_for_alternatives() {
4133        let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
4134        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4135        // Should find both the alternative entry and python3-dev is not included
4136        assert_eq!(entries.len(), 1);
4137        assert_eq!(entries[0].0, 0);
4138    }
4139
4140    #[test]
4141    fn test_iter_relations_for_not_found() {
4142        let relations: Relations = "python3, rustc".parse().unwrap();
4143        let entries: Vec<_> = relations.iter_relations_for("debhelper").collect();
4144        assert_eq!(entries.len(), 0);
4145    }
4146
4147    #[test]
4148    fn test_has_relation_exists() {
4149        let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4150        assert!(relations.has_relation("debhelper"));
4151        assert!(relations.has_relation("python3"));
4152        assert!(relations.has_relation("rustc"));
4153    }
4154
4155    #[test]
4156    fn test_has_relation_not_exists() {
4157        let relations: Relations = "python3, rustc".parse().unwrap();
4158        assert!(!relations.has_relation("debhelper"));
4159    }
4160
4161    #[test]
4162    fn test_has_relation_in_alternative() {
4163        let relations: Relations = "python3 | python3-minimal".parse().unwrap();
4164        assert!(relations.has_relation("python3"));
4165        assert!(relations.has_relation("python3-minimal"));
4166    }
4167
4168    #[test]
4169    fn test_sorting_order_wrap_and_sort_build_systems() {
4170        let order = WrapAndSortOrder;
4171        // Build systems should come before regular packages
4172        assert!(order.lt("debhelper", "python3"));
4173        assert!(order.lt("debhelper-compat", "rustc"));
4174        assert!(order.lt("cdbs", "aaa"));
4175        assert!(order.lt("dh-python", "python3"));
4176    }
4177
4178    #[test]
4179    fn test_sorting_order_wrap_and_sort_regular_packages() {
4180        let order = WrapAndSortOrder;
4181        // Regular packages sorted alphabetically
4182        assert!(order.lt("aaa", "bbb"));
4183        assert!(order.lt("python3", "rustc"));
4184        assert!(!order.lt("rustc", "python3"));
4185    }
4186
4187    #[test]
4188    fn test_sorting_order_wrap_and_sort_substvars() {
4189        let order = WrapAndSortOrder;
4190        // Substvars should come after regular packages
4191        assert!(order.lt("python3", "${misc:Depends}"));
4192        assert!(!order.lt("${misc:Depends}", "python3"));
4193        // But wrap-and-sort doesn't ignore them
4194        assert!(!order.ignore("${misc:Depends}"));
4195    }
4196
4197    #[test]
4198    fn test_sorting_order_default_special_items() {
4199        let order = DefaultSortingOrder;
4200        // Special items should come after regular items
4201        assert!(order.lt("python3", "${misc:Depends}"));
4202        assert!(order.lt("aaa", "@cdbs@"));
4203        // And should be ignored
4204        assert!(order.ignore("${misc:Depends}"));
4205        assert!(order.ignore("@cdbs@"));
4206        assert!(!order.ignore("python3"));
4207    }
4208
4209    #[test]
4210    fn test_is_special_package_name() {
4211        assert!(is_special_package_name("${misc:Depends}"));
4212        assert!(is_special_package_name("${shlibs:Depends}"));
4213        assert!(is_special_package_name("@cdbs@"));
4214        assert!(!is_special_package_name("python3"));
4215        assert!(!is_special_package_name("debhelper"));
4216    }
4217
4218    #[test]
4219    fn test_add_dependency_with_explicit_position() {
4220        // Test that add_dependency works with explicit position and preserves whitespace
4221        let mut relations: Relations = "python3,  rustc".parse().unwrap();
4222        let entry = Entry::from(Relation::simple("debhelper"));
4223        relations.add_dependency(entry, Some(1));
4224        // Should preserve the 2-space pattern from the original
4225        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc");
4226    }
4227
4228    #[test]
4229    fn test_whitespace_detection_single_space() {
4230        let mut relations: Relations = "python3, rustc".parse().unwrap();
4231        let entry = Entry::from(Relation::simple("debhelper"));
4232        relations.add_dependency(entry, Some(1));
4233        assert_eq!(relations.to_string(), "python3, debhelper, rustc");
4234    }
4235
4236    #[test]
4237    fn test_whitespace_detection_multiple_spaces() {
4238        let mut relations: Relations = "python3,  rustc,  gcc".parse().unwrap();
4239        let entry = Entry::from(Relation::simple("debhelper"));
4240        relations.add_dependency(entry, Some(1));
4241        // Should detect and use the 2-space pattern
4242        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc,  gcc");
4243    }
4244
4245    #[test]
4246    fn test_whitespace_detection_mixed_patterns() {
4247        // When patterns differ, use the most common one
4248        let mut relations: Relations = "a, b, c,  d, e".parse().unwrap();
4249        let entry = Entry::from(Relation::simple("x"));
4250        relations.push(entry);
4251        // Three single-space (after a, b, d), one double-space (after c)
4252        // Should use single space as it's most common
4253        assert_eq!(relations.to_string(), "a, b, c,  d, e, x");
4254    }
4255
4256    #[test]
4257    fn test_whitespace_detection_newlines() {
4258        let mut relations: Relations = "python3,\n rustc".parse().unwrap();
4259        let entry = Entry::from(Relation::simple("debhelper"));
4260        relations.add_dependency(entry, Some(1));
4261        // Detects full pattern including newline
4262        assert_eq!(relations.to_string(), "python3,\n debhelper,\n rustc");
4263    }
4264
4265    #[test]
4266    fn test_append_with_newline_no_trailing() {
4267        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4268        let entry = Entry::from(Relation::simple("blah"));
4269        relations.add_dependency(entry, None);
4270        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4271    }
4272
4273    #[test]
4274    fn test_append_with_trailing_newline() {
4275        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4276        let entry = Entry::from(Relation::simple("blah"));
4277        relations.add_dependency(entry, None);
4278        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4279    }
4280
4281    #[test]
4282    fn test_append_with_4_space_indent() {
4283        let mut relations: Relations = "foo,\n    bar".parse().unwrap();
4284        let entry = Entry::from(Relation::simple("blah"));
4285        relations.add_dependency(entry, None);
4286        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4287    }
4288
4289    #[test]
4290    fn test_append_with_4_space_and_trailing_newline() {
4291        let mut relations: Relations = "foo,\n    bar\n".parse().unwrap();
4292        let entry = Entry::from(Relation::simple("blah"));
4293        relations.add_dependency(entry, None);
4294        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4295    }
4296
4297    #[test]
4298    fn test_odd_syntax_append_no_trailing() {
4299        let mut relations: Relations = "\n foo\n , bar".parse().unwrap();
4300        let entry = Entry::from(Relation::simple("blah"));
4301        relations.add_dependency(entry, None);
4302        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4303    }
4304
4305    #[test]
4306    fn test_odd_syntax_append_with_trailing() {
4307        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4308        let entry = Entry::from(Relation::simple("blah"));
4309        relations.add_dependency(entry, None);
4310        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4311    }
4312
4313    #[test]
4314    fn test_insert_at_1_no_trailing() {
4315        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4316        let entry = Entry::from(Relation::simple("blah"));
4317        relations.add_dependency(entry, Some(1));
4318        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4319    }
4320
4321    #[test]
4322    fn test_insert_at_1_with_trailing() {
4323        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4324        let entry = Entry::from(Relation::simple("blah"));
4325        relations.add_dependency(entry, Some(1));
4326        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4327    }
4328
4329    #[test]
4330    fn test_odd_syntax_insert_at_1() {
4331        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4332        let entry = Entry::from(Relation::simple("blah"));
4333        relations.add_dependency(entry, Some(1));
4334        assert_eq!(relations.to_string(), "\n foo\n , blah\n , bar");
4335    }
4336
4337    #[test]
4338    fn test_relations_preserves_exact_whitespace() {
4339        // Test that Relations preserves exact whitespace from input
4340        let input =
4341            "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], pkg-config";
4342
4343        let relations: Relations = input.parse().unwrap();
4344
4345        // The whitespace should be preserved in the syntax tree
4346        assert_eq!(
4347            relations.to_string(),
4348            input,
4349            "Relations should preserve exact whitespace from input"
4350        );
4351    }
4352
4353    #[test]
4354    fn test_remove_entry_preserves_indentation() {
4355        // Test that removing an entry preserves the indentation pattern
4356        let input = "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config";
4357
4358        let mut relations: Relations = input.parse().unwrap();
4359
4360        // Find and remove dh-systemd entry (index 2)
4361        let mut to_remove = Vec::new();
4362        for (idx, entry) in relations.entries().enumerate() {
4363            for relation in entry.relations() {
4364                if relation.name() == "dh-systemd" {
4365                    to_remove.push(idx);
4366                    break;
4367                }
4368            }
4369        }
4370
4371        for idx in to_remove.into_iter().rev() {
4372            relations.remove_entry(idx);
4373        }
4374
4375        let output = relations.to_string();
4376        println!("After removal: '{}'", output);
4377
4378        // The 4-space indentation should be preserved
4379        assert!(
4380            output.contains("\n    libsystemd-dev"),
4381            "Expected 4-space indentation to be preserved, but got:\n'{}'",
4382            output
4383        );
4384    }
4385
4386    #[test]
4387    fn test_relation_is_implied_by_same_package() {
4388        // Same package name with compatible version constraints
4389        let inner = Relation::new(
4390            "pkg",
4391            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4392        );
4393        let outer = Relation::new(
4394            "pkg",
4395            Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())),
4396        );
4397        assert!(inner.is_implied_by(&outer));
4398    }
4399
4400    #[test]
4401    fn test_relation_is_implied_by_different_package() {
4402        // Different package names should not imply
4403        let inner = Relation::new("pkg1", None);
4404        let outer = Relation::new("pkg2", None);
4405        assert!(!inner.is_implied_by(&outer));
4406    }
4407
4408    #[test]
4409    fn test_relation_is_implied_by_no_version() {
4410        // No version constraint is implied by any version
4411        let inner = Relation::new("pkg", None);
4412        let outer = Relation::new(
4413            "pkg",
4414            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4415        );
4416        assert!(inner.is_implied_by(&outer));
4417    }
4418
4419    #[test]
4420    fn test_relation_is_implied_by_identical() {
4421        // Identical relations imply each other
4422        let inner = Relation::new(
4423            "pkg",
4424            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4425        );
4426        let outer = Relation::new(
4427            "pkg",
4428            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4429        );
4430        assert!(inner.is_implied_by(&outer));
4431        assert!(outer.is_implied_by(&inner));
4432    }
4433
4434    #[test]
4435    fn test_relation_is_implied_by_greater_than_equal() {
4436        // pkg >= 1.0 is implied by pkg >= 2.0
4437        let inner = Relation::new(
4438            "pkg",
4439            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4440        );
4441        let outer = Relation::new(
4442            "pkg",
4443            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
4444        );
4445        assert!(inner.is_implied_by(&outer));
4446        assert!(!outer.is_implied_by(&inner));
4447
4448        // pkg >= 1.0 is implied by pkg = 2.0
4449        let outer = Relation::new(
4450            "pkg",
4451            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4452        );
4453        assert!(inner.is_implied_by(&outer));
4454
4455        // pkg >= 1.0 is implied by pkg >> 1.5
4456        let outer = Relation::new(
4457            "pkg",
4458            Some((VersionConstraint::GreaterThan, "1.5".parse().unwrap())),
4459        );
4460        assert!(inner.is_implied_by(&outer));
4461
4462        // pkg >= 3.0 is NOT implied by pkg >> 3.0 (>> 3.0 doesn't include 3.0 itself)
4463        let inner = Relation::new(
4464            "pkg",
4465            Some((VersionConstraint::GreaterThanEqual, "3.0".parse().unwrap())),
4466        );
4467        let outer = Relation::new(
4468            "pkg",
4469            Some((VersionConstraint::GreaterThan, "3.0".parse().unwrap())),
4470        );
4471        assert!(!inner.is_implied_by(&outer));
4472    }
4473
4474    #[test]
4475    fn test_relation_is_implied_by_less_than_equal() {
4476        // pkg <= 2.0 is implied by pkg <= 1.0
4477        let inner = Relation::new(
4478            "pkg",
4479            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4480        );
4481        let outer = Relation::new(
4482            "pkg",
4483            Some((VersionConstraint::LessThanEqual, "1.0".parse().unwrap())),
4484        );
4485        assert!(inner.is_implied_by(&outer));
4486        assert!(!outer.is_implied_by(&inner));
4487
4488        // pkg <= 2.0 is implied by pkg = 1.0
4489        let outer = Relation::new(
4490            "pkg",
4491            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4492        );
4493        assert!(inner.is_implied_by(&outer));
4494
4495        // pkg <= 2.0 is implied by pkg << 1.5
4496        let outer = Relation::new(
4497            "pkg",
4498            Some((VersionConstraint::LessThan, "1.5".parse().unwrap())),
4499        );
4500        assert!(inner.is_implied_by(&outer));
4501    }
4502
4503    #[test]
4504    fn test_relation_is_implied_by_equal() {
4505        // pkg = 1.0 is only implied by pkg = 1.0
4506        let inner = Relation::new(
4507            "pkg",
4508            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4509        );
4510        let outer = Relation::new(
4511            "pkg",
4512            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4513        );
4514        assert!(inner.is_implied_by(&outer));
4515
4516        // Not implied by different version
4517        let outer = Relation::new(
4518            "pkg",
4519            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4520        );
4521        assert!(!inner.is_implied_by(&outer));
4522
4523        // Not implied by >= constraint
4524        let outer = Relation::new(
4525            "pkg",
4526            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4527        );
4528        assert!(!inner.is_implied_by(&outer));
4529    }
4530
4531    #[test]
4532    fn test_relation_is_implied_by_greater_than() {
4533        // pkg >> 1.0 is implied by pkg >> 2.0
4534        let inner = Relation::new(
4535            "pkg",
4536            Some((VersionConstraint::GreaterThan, "1.0".parse().unwrap())),
4537        );
4538        let outer = Relation::new(
4539            "pkg",
4540            Some((VersionConstraint::GreaterThan, "2.0".parse().unwrap())),
4541        );
4542        assert!(inner.is_implied_by(&outer));
4543
4544        // pkg >> 1.0 is implied by pkg = 2.0
4545        let outer = Relation::new(
4546            "pkg",
4547            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4548        );
4549        assert!(inner.is_implied_by(&outer));
4550
4551        // pkg >> 1.0 is implied by pkg >= 1.5 (strictly greater)
4552        let outer = Relation::new(
4553            "pkg",
4554            Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())),
4555        );
4556        assert!(inner.is_implied_by(&outer));
4557
4558        // pkg >> 1.0 is NOT implied by pkg >= 1.0 (could be equal)
4559        let outer = Relation::new(
4560            "pkg",
4561            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4562        );
4563        assert!(!inner.is_implied_by(&outer));
4564    }
4565
4566    #[test]
4567    fn test_relation_is_implied_by_less_than() {
4568        // pkg << 2.0 is implied by pkg << 1.0
4569        let inner = Relation::new(
4570            "pkg",
4571            Some((VersionConstraint::LessThan, "2.0".parse().unwrap())),
4572        );
4573        let outer = Relation::new(
4574            "pkg",
4575            Some((VersionConstraint::LessThan, "1.0".parse().unwrap())),
4576        );
4577        assert!(inner.is_implied_by(&outer));
4578
4579        // pkg << 2.0 is implied by pkg = 1.0
4580        let outer = Relation::new(
4581            "pkg",
4582            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4583        );
4584        assert!(inner.is_implied_by(&outer));
4585
4586        // pkg << 2.0 is implied by pkg <= 1.5 (strictly less)
4587        let outer = Relation::new(
4588            "pkg",
4589            Some((VersionConstraint::LessThanEqual, "1.5".parse().unwrap())),
4590        );
4591        assert!(inner.is_implied_by(&outer));
4592    }
4593
4594    #[test]
4595    fn test_relation_is_implied_by_incompatible_constraints() {
4596        // >= and <= are incompatible
4597        let inner = Relation::new(
4598            "pkg",
4599            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4600        );
4601        let outer = Relation::new(
4602            "pkg",
4603            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4604        );
4605        assert!(!inner.is_implied_by(&outer));
4606        assert!(!outer.is_implied_by(&inner));
4607    }
4608
4609    #[test]
4610    fn test_entry_is_implied_by_identical() {
4611        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4612        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4613        assert!(inner.is_implied_by(&outer));
4614    }
4615
4616    #[test]
4617    fn test_entry_is_implied_by_or_group() {
4618        // "pkg >= 1.0" is implied by "pkg >= 1.5 | libc6"
4619        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4620        let outer: Entry = "pkg (>= 1.5) | libc6".parse().unwrap();
4621        assert!(inner.is_implied_by(&outer));
4622    }
4623
4624    #[test]
4625    fn test_entry_is_implied_by_simple_or() {
4626        // "pkg1 | pkg2" is implied by "pkg1" (first alternative satisfies)
4627        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4628        let outer: Entry = "pkg1".parse().unwrap();
4629        assert!(inner.is_implied_by(&outer));
4630
4631        // Also implied by "pkg2"
4632        let outer: Entry = "pkg2".parse().unwrap();
4633        assert!(inner.is_implied_by(&outer));
4634    }
4635
4636    #[test]
4637    fn test_entry_is_implied_by_not_implied() {
4638        // "pkg >= 2.0" is NOT implied by "pkg >= 1.0"
4639        let inner: Entry = "pkg (>= 2.0)".parse().unwrap();
4640        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4641        assert!(!inner.is_implied_by(&outer));
4642    }
4643
4644    #[test]
4645    fn test_entry_is_implied_by_different_packages() {
4646        let inner: Entry = "pkg1".parse().unwrap();
4647        let outer: Entry = "pkg2".parse().unwrap();
4648        assert!(!inner.is_implied_by(&outer));
4649    }
4650
4651    #[test]
4652    fn test_entry_is_implied_by_complex_or() {
4653        // "pkg1 | pkg2" is implied by "pkg1 | pkg2" (identical)
4654        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4655        let outer: Entry = "pkg1 | pkg2".parse().unwrap();
4656        assert!(inner.is_implied_by(&outer));
4657
4658        // "pkg1 | pkg2" is implied by "pkg1 | pkg2 | pkg3" (one matches)
4659        let outer: Entry = "pkg1 | pkg2 | pkg3".parse().unwrap();
4660        assert!(inner.is_implied_by(&outer));
4661    }
4662
4663    #[test]
4664    fn test_parse_version_with_epoch() {
4665        // Test parsing version strings with epoch (e.g., "1:2.3.2-2~")
4666        // The colon should be treated as part of the version, not as a delimiter
4667        let input = "amule-dbg (<< 1:2.3.2-2~)";
4668        let parsed: Relations = input.parse().unwrap();
4669        assert_eq!(parsed.to_string(), input);
4670        assert_eq!(parsed.entries().count(), 1);
4671        let entry = parsed.entries().next().unwrap();
4672        assert_eq!(entry.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4673        assert_eq!(entry.relations().count(), 1);
4674        let relation = entry.relations().next().unwrap();
4675        assert_eq!(relation.name(), "amule-dbg");
4676        assert_eq!(relation.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4677        assert_eq!(
4678            relation.version(),
4679            Some((VersionConstraint::LessThan, "1:2.3.2-2~".parse().unwrap()))
4680        );
4681    }
4682}