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            // Check if there are any substvars
817            if self.substvars().next().is_some() {
818                // Has substvars but no entries - use default spacing
819                return default.to_string();
820            }
821            return String::from(""); // Truly empty - first entry gets no prefix
822        }
823
824        if num_entries == 1 {
825            // Single entry - check if there's a pattern after it
826            if let Some(node) = entries[0].0.next_sibling_or_token() {
827                if node.kind() == COMMA {
828                    let pattern = Self::collect_whitespace(node.next_sibling_or_token());
829                    if !pattern.is_empty() {
830                        return pattern;
831                    }
832                }
833            }
834            return default.to_string(); // Use default for single entry with no pattern
835        }
836
837        // Count whitespace patterns after commas (excluding the last entry)
838        let mut whitespace_counts: HashMap<String, usize> = HashMap::new();
839
840        for (i, entry) in entries.iter().enumerate() {
841            if i == num_entries - 1 {
842                break; // Skip the last entry
843            }
844
845            // Look for comma and whitespace after this entry
846            if let Some(mut node) = entry.0.next_sibling_or_token() {
847                // Skip any whitespace/newlines before the comma (odd syntax)
848                while matches!(node.kind(), WHITESPACE | NEWLINE) {
849                    if let Some(next) = node.next_sibling_or_token() {
850                        node = next;
851                    } else {
852                        break;
853                    }
854                }
855
856                // Found comma, collect all whitespace/newlines after it
857                if node.kind() == COMMA {
858                    let pattern = Self::collect_whitespace(node.next_sibling_or_token());
859                    if !pattern.is_empty() {
860                        *whitespace_counts.entry(pattern).or_insert(0) += 1;
861                    }
862                }
863            }
864        }
865
866        // If there's exactly one pattern, use it
867        if whitespace_counts.len() == 1 {
868            if let Some((ws, _)) = whitespace_counts.iter().next() {
869                return ws.clone();
870            }
871        }
872
873        // Multiple patterns - use the most common
874        if let Some((ws, _)) = whitespace_counts.iter().max_by_key(|(_, count)| *count) {
875            return ws.clone();
876        }
877
878        // Use the provided default
879        default.to_string()
880    }
881
882    /// Insert a new entry at the given index
883    ///
884    /// # Arguments
885    /// * `idx` - The index to insert at
886    /// * `entry` - The entry to insert
887    /// * `default_sep` - Optional default separator to use if no pattern is detected (defaults to " ")
888    pub fn insert_with_separator(&mut self, idx: usize, entry: Entry, default_sep: Option<&str>) {
889        let is_empty = self.entries().next().is_none();
890        let whitespace = self.detect_whitespace_pattern(default_sep.unwrap_or(" "));
891
892        // Strip trailing whitespace first
893        self.strip_trailing_whitespace();
894
895        // Detect odd syntax (whitespace before comma)
896        let odd_syntax = self.detect_odd_syntax();
897
898        let (position, new_children) = if let Some(current_entry) = self.entries().nth(idx) {
899            let to_insert = if idx == 0 && is_empty {
900                vec![entry.0.green().into()]
901            } else if let Some((before_ws, after_ws)) = &odd_syntax {
902                let mut nodes = vec![entry.0.green().into()];
903                nodes.extend(Self::build_odd_syntax_nodes(before_ws, after_ws));
904                nodes
905            } else {
906                vec![
907                    entry.0.green().into(),
908                    NodeOrToken::Token(GreenToken::new(COMMA.into(), ",")),
909                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), whitespace.as_str())),
910                ]
911            };
912
913            (current_entry.0.index(), to_insert)
914        } else {
915            let child_count = self.0.children_with_tokens().count();
916            let to_insert = if idx == 0 {
917                vec![entry.0.green().into()]
918            } else if let Some((before_ws, after_ws)) = &odd_syntax {
919                let mut nodes = Self::build_odd_syntax_nodes(before_ws, after_ws);
920                nodes.push(entry.0.green().into());
921                nodes
922            } else {
923                vec![
924                    NodeOrToken::Token(GreenToken::new(COMMA.into(), ",")),
925                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), whitespace.as_str())),
926                    entry.0.green().into(),
927                ]
928            };
929
930            (child_count, to_insert)
931        };
932        // We can safely replace the root here since Relations is a root node
933        self.0 = SyntaxNode::new_root_mut(
934            self.0.replace_with(
935                self.0
936                    .green()
937                    .splice_children(position..position, new_children),
938            ),
939        );
940    }
941
942    /// Insert a new entry at the given index with default separator
943    pub fn insert(&mut self, idx: usize, entry: Entry) {
944        self.insert_with_separator(idx, entry, None);
945    }
946
947    /// Helper to recursively strip trailing whitespace from an ENTRY node
948    fn strip_entry_trailing_ws(entry: &SyntaxNode) -> GreenNode {
949        let mut children: Vec<_> = entry
950            .children_with_tokens()
951            .map(|c| Self::to_green(&c))
952            .collect();
953
954        // Strip trailing whitespace from the last RELATION if present
955        if let Some(NodeOrToken::Node(last)) = children.last() {
956            if last.kind() == rowan::SyntaxKind(RELATION as u16) {
957                // Replace last child with stripped version
958                let relation_node = entry.children().last().unwrap();
959                children.pop();
960                children.push(NodeOrToken::Node(Self::strip_relation_trailing_ws(
961                    &relation_node,
962                )));
963            }
964        }
965
966        // Strip trailing whitespace tokens at entry level
967        let stripped = Self::strip_trailing_ws_from_children(children);
968        GreenNode::new(ENTRY.into(), stripped)
969    }
970
971    fn strip_trailing_whitespace(&mut self) {
972        let mut children: Vec<_> = self
973            .0
974            .children_with_tokens()
975            .map(|c| Self::to_green(&c))
976            .collect();
977
978        // Strip trailing whitespace from the last ENTRY if present
979        if let Some(NodeOrToken::Node(last)) = children.last() {
980            if last.kind() == rowan::SyntaxKind(ENTRY as u16) {
981                let last_entry = self.0.children().last().unwrap();
982                children.pop();
983                children.push(NodeOrToken::Node(Self::strip_entry_trailing_ws(
984                    &last_entry,
985                )));
986            }
987        }
988
989        // Strip trailing whitespace tokens at root level
990        let stripped = Self::strip_trailing_ws_from_children(children);
991
992        let nc = self.0.children_with_tokens().count();
993        self.0 = SyntaxNode::new_root_mut(
994            self.0
995                .replace_with(self.0.green().splice_children(0..nc, stripped)),
996        );
997    }
998
999    /// Replace the entry at the given index
1000    pub fn replace(&mut self, idx: usize, entry: Entry) {
1001        let current_entry = self.get_entry(idx).unwrap();
1002        self.0.splice_children(
1003            current_entry.0.index()..current_entry.0.index() + 1,
1004            vec![entry.0.into()],
1005        );
1006    }
1007
1008    /// Push a new entry to the relations field
1009    pub fn push(&mut self, entry: Entry) {
1010        let pos = self.entries().count();
1011        self.insert(pos, entry);
1012    }
1013
1014    /// Return the names of substvars in this relations field
1015    pub fn substvars(&self) -> impl Iterator<Item = String> + '_ {
1016        self.0
1017            .children()
1018            .filter_map(Substvar::cast)
1019            .map(|s| s.to_string())
1020    }
1021
1022    /// Parse a relations field from a string, allowing syntax errors
1023    pub fn parse_relaxed(s: &str, allow_substvar: bool) -> (Relations, Vec<String>) {
1024        let parse = parse(s, allow_substvar);
1025        (parse.root_mut(), parse.errors)
1026    }
1027
1028    /// Check if this relations field is satisfied by the given package versions.
1029    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
1030        self.entries().all(|e| e.satisfied_by(package_version))
1031    }
1032
1033    /// Check if this relations field is empty
1034    pub fn is_empty(&self) -> bool {
1035        self.entries().count() == 0
1036    }
1037
1038    /// Get the number of entries in this relations field
1039    pub fn len(&self) -> usize {
1040        self.entries().count()
1041    }
1042
1043    /// Ensure that a package has at least a minimum version constraint.
1044    ///
1045    /// If the package already exists with a version constraint that satisfies
1046    /// the minimum version, it is left unchanged. Otherwise, the constraint
1047    /// is updated or added.
1048    ///
1049    /// # Arguments
1050    /// * `package` - The package name
1051    /// * `minimum_version` - The minimum version required
1052    ///
1053    /// # Example
1054    /// ```
1055    /// use debian_control::lossless::relations::Relations;
1056    ///
1057    /// let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
1058    /// relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
1059    /// assert_eq!(relations.to_string(), "debhelper (>= 12)");
1060    ///
1061    /// let mut relations: Relations = "python3".parse().unwrap();
1062    /// relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
1063    /// assert!(relations.to_string().contains("debhelper (>= 12)"));
1064    /// ```
1065    pub fn ensure_minimum_version(&mut self, package: &str, minimum_version: &Version) {
1066        let mut found = false;
1067        let mut obsolete_indices = vec![];
1068        let mut update_idx = None;
1069
1070        let entries: Vec<_> = self.entries().collect();
1071        for (idx, entry) in entries.iter().enumerate() {
1072            let relations: Vec<_> = entry.relations().collect();
1073
1074            // Check if this entry has multiple alternatives with our package
1075            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1076            if names.len() > 1 && names.contains(&package.to_string()) {
1077                // This is a complex alternative relation, mark for removal if obsolete
1078                let is_obsolete = relations.iter().any(|r| {
1079                    if r.name() != package {
1080                        return false;
1081                    }
1082                    if let Some((vc, ver)) = r.version() {
1083                        matches!(vc, VersionConstraint::GreaterThan if &ver < minimum_version)
1084                            || matches!(vc, VersionConstraint::GreaterThanEqual if &ver <= minimum_version)
1085                    } else {
1086                        false
1087                    }
1088                });
1089                if is_obsolete {
1090                    obsolete_indices.push(idx);
1091                }
1092                continue;
1093            }
1094
1095            // Single package entry
1096            if names.len() == 1 && names[0] == package {
1097                found = true;
1098                let relation = relations.into_iter().next().unwrap();
1099
1100                // Check if update is needed
1101                let should_update = if let Some((vc, ver)) = relation.version() {
1102                    match vc {
1103                        VersionConstraint::GreaterThanEqual | VersionConstraint::GreaterThan => {
1104                            &ver < minimum_version
1105                        }
1106                        _ => false,
1107                    }
1108                } else {
1109                    true
1110                };
1111
1112                if should_update {
1113                    update_idx = Some(idx);
1114                }
1115                break;
1116            }
1117        }
1118
1119        // Perform updates after iteration
1120        if let Some(idx) = update_idx {
1121            let relation = Relation::new(
1122                package,
1123                Some((VersionConstraint::GreaterThanEqual, minimum_version.clone())),
1124            );
1125            // Get the existing entry and replace its relation to preserve formatting
1126            let mut entry = self.get_entry(idx).unwrap();
1127            entry.replace(0, relation);
1128            self.replace(idx, entry);
1129        }
1130
1131        // Remove obsolete entries
1132        for idx in obsolete_indices.into_iter().rev() {
1133            self.remove_entry(idx);
1134        }
1135
1136        // Add if not found
1137        if !found {
1138            let relation = Relation::new(
1139                package,
1140                Some((VersionConstraint::GreaterThanEqual, minimum_version.clone())),
1141            );
1142            self.push(Entry::from(relation));
1143        }
1144    }
1145
1146    /// Ensure that a package has an exact version constraint.
1147    ///
1148    /// # Arguments
1149    /// * `package` - The package name
1150    /// * `version` - The exact version required
1151    ///
1152    /// # Example
1153    /// ```
1154    /// use debian_control::lossless::relations::Relations;
1155    ///
1156    /// let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
1157    /// relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
1158    /// assert_eq!(relations.to_string(), "debhelper (= 12)");
1159    /// ```
1160    pub fn ensure_exact_version(&mut self, package: &str, version: &Version) {
1161        let mut found = false;
1162        let mut update_idx = None;
1163
1164        let entries: Vec<_> = self.entries().collect();
1165        for (idx, entry) in entries.iter().enumerate() {
1166            let relations: Vec<_> = entry.relations().collect();
1167            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1168
1169            if names.len() > 1 && names[0] == package {
1170                panic!("Complex rule for {}, aborting", package);
1171            }
1172
1173            if names.len() == 1 && names[0] == package {
1174                found = true;
1175                let relation = relations.into_iter().next().unwrap();
1176
1177                let should_update = if let Some((vc, ver)) = relation.version() {
1178                    vc != VersionConstraint::Equal || &ver != version
1179                } else {
1180                    true
1181                };
1182
1183                if should_update {
1184                    update_idx = Some(idx);
1185                }
1186                break;
1187            }
1188        }
1189
1190        // Perform update after iteration
1191        if let Some(idx) = update_idx {
1192            let relation =
1193                Relation::new(package, Some((VersionConstraint::Equal, version.clone())));
1194            // Get the existing entry and replace its relation to preserve formatting
1195            let mut entry = self.get_entry(idx).unwrap();
1196            entry.replace(0, relation);
1197            self.replace(idx, entry);
1198        }
1199
1200        if !found {
1201            let relation =
1202                Relation::new(package, Some((VersionConstraint::Equal, version.clone())));
1203            self.push(Entry::from(relation));
1204        }
1205    }
1206
1207    /// Ensure that a package dependency exists, without specifying a version.
1208    ///
1209    /// If the package already exists (with or without a version constraint),
1210    /// the relations field is left unchanged. Otherwise, the package is added
1211    /// without a version constraint.
1212    ///
1213    /// # Arguments
1214    /// * `package` - The package name
1215    ///
1216    /// # Example
1217    /// ```
1218    /// use debian_control::lossless::relations::Relations;
1219    ///
1220    /// let mut relations: Relations = "python3".parse().unwrap();
1221    /// relations.ensure_some_version("debhelper");
1222    /// assert!(relations.to_string().contains("debhelper"));
1223    /// ```
1224    pub fn ensure_some_version(&mut self, package: &str) {
1225        for entry in self.entries() {
1226            let relations: Vec<_> = entry.relations().collect();
1227            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1228
1229            if names.len() > 1 && names[0] == package {
1230                panic!("Complex rule for {}, aborting", package);
1231            }
1232
1233            if names.len() == 1 && names[0] == package {
1234                // Package already exists, don't modify
1235                return;
1236            }
1237        }
1238
1239        // Package not found, add it
1240        let relation = Relation::simple(package);
1241        self.push(Entry::from(relation));
1242    }
1243
1244    /// Ensure that a relation exists in the dependencies.
1245    ///
1246    /// This function checks if the provided entry is already satisfied by an
1247    /// existing entry. If it is, no changes are made. If an existing entry is
1248    /// weaker than the new entry (i.e., the new entry implies the existing one),
1249    /// the existing entry is replaced with the new one. Otherwise, the new entry
1250    /// is added.
1251    ///
1252    /// # Arguments
1253    /// * `new_entry` - The entry to ensure exists
1254    ///
1255    /// # Returns
1256    /// `true` if the entry was added or replaced, `false` if it was already satisfied
1257    ///
1258    /// # Example
1259    /// ```
1260    /// use debian_control::lossless::relations::{Relations, Entry};
1261    ///
1262    /// let mut relations: Relations = "python3".parse().unwrap();
1263    /// let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
1264    /// let added = relations.ensure_relation(new_entry);
1265    /// assert!(added);
1266    /// assert!(relations.to_string().contains("debhelper (>= 12)"));
1267    /// ```
1268    pub fn ensure_relation(&mut self, new_entry: Entry) -> bool {
1269        let mut to_replace: Vec<usize> = Vec::new();
1270        let mut to_remove: Vec<usize> = Vec::new();
1271        let mut already_satisfied = false;
1272
1273        // Check existing entries
1274        for (idx, existing_entry) in self.entries().enumerate() {
1275            if new_entry.is_implied_by(&existing_entry) {
1276                // The new entry is already satisfied by an existing entry
1277                already_satisfied = true;
1278                break;
1279            }
1280            if existing_entry.is_implied_by(&new_entry) {
1281                // The new entry implies the existing one (is stronger)
1282                // We should replace/remove the weaker existing entry
1283                if to_replace.is_empty() {
1284                    to_replace.push(idx);
1285                } else {
1286                    to_remove.push(idx);
1287                }
1288            }
1289        }
1290
1291        if already_satisfied {
1292            return false;
1293        }
1294
1295        // Remove weaker entries in reverse order
1296        for idx in to_remove.into_iter().rev() {
1297            self.remove_entry(idx);
1298        }
1299
1300        // Replace or add the entry
1301        if let Some(&idx) = to_replace.first() {
1302            self.replace(idx, new_entry);
1303        } else {
1304            self.add_dependency(new_entry, None);
1305        }
1306
1307        true
1308    }
1309
1310    /// Ensure that a substitution variable is present in the relations.
1311    ///
1312    /// If the substvar already exists, it is left unchanged. Otherwise, it is added
1313    /// at the end of the relations list.
1314    ///
1315    /// # Arguments
1316    /// * `substvar` - The substitution variable (e.g., "${misc:Depends}")
1317    ///
1318    /// # Returns
1319    /// `Ok(())` on success, or `Err` with an error message if parsing fails
1320    ///
1321    /// # Example
1322    /// ```
1323    /// use debian_control::lossless::relations::Relations;
1324    ///
1325    /// let mut relations: Relations = "python3".parse().unwrap();
1326    /// relations.ensure_substvar("${misc:Depends}").unwrap();
1327    /// assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
1328    /// ```
1329    pub fn ensure_substvar(&mut self, substvar: &str) -> Result<(), String> {
1330        // Check if the substvar already exists
1331        for existing in self.substvars() {
1332            if existing.trim() == substvar.trim() {
1333                return Ok(());
1334            }
1335        }
1336
1337        // Parse the substvar
1338        let (parsed, errors) = Relations::parse_relaxed(substvar, true);
1339        if !errors.is_empty() {
1340            return Err(errors.join("\n"));
1341        }
1342
1343        // Detect whitespace pattern to preserve formatting
1344        let whitespace = self.detect_whitespace_pattern(" ");
1345
1346        // Find the substvar node and inject it
1347        for substvar_node in parsed.0.children().filter(|n| n.kind() == SUBSTVAR) {
1348            let has_content = self.entries().next().is_some() || self.substvars().next().is_some();
1349
1350            let mut builder = GreenNodeBuilder::new();
1351            builder.start_node(ROOT.into());
1352
1353            // Copy existing content
1354            for child in self.0.children_with_tokens() {
1355                match child {
1356                    NodeOrToken::Node(n) => inject(&mut builder, n),
1357                    NodeOrToken::Token(t) => builder.token(t.kind().into(), t.text()),
1358                }
1359            }
1360
1361            // Add separator if needed, using detected whitespace pattern
1362            if has_content {
1363                builder.token(COMMA.into(), ",");
1364                builder.token(WHITESPACE.into(), whitespace.as_str());
1365            }
1366
1367            // Inject the substvar node
1368            inject(&mut builder, substvar_node);
1369
1370            builder.finish_node();
1371            self.0 = SyntaxNode::new_root_mut(builder.finish());
1372        }
1373
1374        Ok(())
1375    }
1376
1377    /// Remove a substitution variable from the relations.
1378    ///
1379    /// If the substvar exists, it is removed along with its surrounding separators.
1380    /// If the substvar does not exist, this is a no-op.
1381    ///
1382    /// # Arguments
1383    /// * `substvar` - The substitution variable to remove (e.g., "${misc:Depends}")
1384    ///
1385    /// # Example
1386    /// ```
1387    /// use debian_control::lossless::relations::Relations;
1388    ///
1389    /// let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
1390    /// relations.drop_substvar("${misc:Depends}");
1391    /// assert_eq!(relations.to_string(), "python3");
1392    /// ```
1393    pub fn drop_substvar(&mut self, substvar: &str) {
1394        // Find all substvar nodes that match the given string
1395        let substvars_to_remove: Vec<_> = self
1396            .0
1397            .children()
1398            .filter_map(Substvar::cast)
1399            .filter(|s| s.to_string().trim() == substvar.trim())
1400            .collect();
1401
1402        for substvar_node in substvars_to_remove {
1403            // Determine if this is the first substvar (no previous ENTRY or SUBSTVAR siblings)
1404            let is_first = !substvar_node
1405                .0
1406                .siblings(Direction::Prev)
1407                .skip(1)
1408                .any(|n| n.kind() == ENTRY || n.kind() == SUBSTVAR);
1409
1410            let mut removed_comma = false;
1411
1412            // Remove whitespace and comma after the substvar
1413            while let Some(n) = substvar_node.0.next_sibling_or_token() {
1414                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1415                    n.detach();
1416                } else if n.kind() == COMMA {
1417                    n.detach();
1418                    removed_comma = true;
1419                    break;
1420                } else {
1421                    break;
1422                }
1423            }
1424
1425            // If not first, remove preceding whitespace and comma
1426            if !is_first {
1427                while let Some(n) = substvar_node.0.prev_sibling_or_token() {
1428                    if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1429                        n.detach();
1430                    } else if !removed_comma && n.kind() == COMMA {
1431                        n.detach();
1432                        break;
1433                    } else {
1434                        break;
1435                    }
1436                }
1437            } else {
1438                // If first and we didn't remove a comma after, clean up any leading whitespace
1439                while let Some(n) = substvar_node.0.next_sibling_or_token() {
1440                    if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1441                        n.detach();
1442                    } else {
1443                        break;
1444                    }
1445                }
1446            }
1447
1448            // Finally, detach the substvar node itself
1449            substvar_node.0.detach();
1450        }
1451    }
1452
1453    /// Filter entries based on a predicate function.
1454    ///
1455    /// # Arguments
1456    /// * `keep` - A function that returns true for entries to keep
1457    ///
1458    /// # Example
1459    /// ```
1460    /// use debian_control::lossless::relations::Relations;
1461    ///
1462    /// let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1463    /// relations.filter_entries(|entry| {
1464    ///     entry.relations().any(|r| r.name().starts_with("python"))
1465    /// });
1466    /// assert_eq!(relations.to_string(), "python3");
1467    /// ```
1468    pub fn filter_entries<F>(&mut self, keep: F)
1469    where
1470        F: Fn(&Entry) -> bool,
1471    {
1472        let indices_to_remove: Vec<_> = self
1473            .entries()
1474            .enumerate()
1475            .filter_map(|(idx, entry)| if keep(&entry) { None } else { Some(idx) })
1476            .collect();
1477
1478        // Remove in reverse order to maintain correct indices
1479        for idx in indices_to_remove.into_iter().rev() {
1480            self.remove_entry(idx);
1481        }
1482    }
1483
1484    /// Check whether the relations are sorted according to a given sorting order.
1485    ///
1486    /// # Arguments
1487    /// * `sorting_order` - The sorting order to check against
1488    ///
1489    /// # Example
1490    /// ```
1491    /// use debian_control::lossless::relations::{Relations, WrapAndSortOrder};
1492    ///
1493    /// let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
1494    /// assert!(relations.is_sorted(&WrapAndSortOrder));
1495    ///
1496    /// let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
1497    /// assert!(!relations.is_sorted(&WrapAndSortOrder));
1498    /// ```
1499    pub fn is_sorted(&self, sorting_order: &impl SortingOrder) -> bool {
1500        let mut last_name: Option<String> = None;
1501        for entry in self.entries() {
1502            // Skip empty entries
1503            let mut relations = entry.relations();
1504            let Some(relation) = relations.next() else {
1505                continue;
1506            };
1507
1508            let name = relation.name();
1509
1510            // Skip items that should be ignored
1511            if sorting_order.ignore(&name) {
1512                continue;
1513            }
1514
1515            // Check if this breaks the sort order
1516            if let Some(ref last) = last_name {
1517                if sorting_order.lt(&name, last) {
1518                    return false;
1519                }
1520            }
1521
1522            last_name = Some(name);
1523        }
1524        true
1525    }
1526
1527    /// Find the position to insert an entry while maintaining sort order.
1528    ///
1529    /// This method detects the current sorting order and returns the appropriate
1530    /// insertion position. If there are fewer than 2 entries, it defaults to
1531    /// WrapAndSortOrder. If no sorting order is detected, it returns the end position.
1532    ///
1533    /// # Arguments
1534    /// * `entry` - The entry to insert
1535    ///
1536    /// # Returns
1537    /// The index where the entry should be inserted
1538    fn find_insert_position(&self, entry: &Entry) -> usize {
1539        // Get the package name from the first relation in the entry
1540        let Some(relation) = entry.relations().next() else {
1541            // Empty entry, just append at the end
1542            return self.len();
1543        };
1544        let package_name = relation.name();
1545
1546        // Count non-empty entries
1547        let count = self.entries().filter(|e| !e.is_empty()).count();
1548
1549        // If there are less than 2 items, default to WrapAndSortOrder
1550        let sorting_order: Box<dyn SortingOrder> = if count < 2 {
1551            Box::new(WrapAndSortOrder)
1552        } else {
1553            // Try to detect which sorting order is being used
1554            // Try WrapAndSortOrder first, then DefaultSortingOrder
1555            if self.is_sorted(&WrapAndSortOrder) {
1556                Box::new(WrapAndSortOrder)
1557            } else if self.is_sorted(&DefaultSortingOrder) {
1558                Box::new(DefaultSortingOrder)
1559            } else {
1560                // No sorting order detected, just append at the end
1561                return self.len();
1562            }
1563        };
1564
1565        // If adding a special item that should be ignored by this sort order, append at the end
1566        if sorting_order.ignore(&package_name) {
1567            return self.len();
1568        }
1569
1570        // Insert in sorted order among regular items
1571        let mut position = 0;
1572        for (idx, existing_entry) in self.entries().enumerate() {
1573            let mut existing_relations = existing_entry.relations();
1574            let Some(existing_relation) = existing_relations.next() else {
1575                // Empty entry, skip
1576                position += 1;
1577                continue;
1578            };
1579
1580            let existing_name = existing_relation.name();
1581
1582            // Skip special items when finding insertion position
1583            if sorting_order.ignore(&existing_name) {
1584                position += 1;
1585                continue;
1586            }
1587
1588            // Compare with regular items only
1589            if sorting_order.lt(&package_name, &existing_name) {
1590                return idx;
1591            }
1592            position += 1;
1593        }
1594
1595        position
1596    }
1597
1598    /// Drop a dependency from the relations by package name.
1599    ///
1600    /// # Arguments
1601    /// * `package` - The package name to remove
1602    ///
1603    /// # Returns
1604    /// `true` if the package was found and removed, `false` otherwise
1605    ///
1606    /// # Example
1607    /// ```
1608    /// use debian_control::lossless::relations::Relations;
1609    ///
1610    /// let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1611    /// assert!(relations.drop_dependency("debhelper"));
1612    /// assert_eq!(relations.to_string(), "python3, rustc");
1613    /// assert!(!relations.drop_dependency("nonexistent"));
1614    /// ```
1615    pub fn drop_dependency(&mut self, package: &str) -> bool {
1616        let indices_to_remove: Vec<_> = self
1617            .entries()
1618            .enumerate()
1619            .filter_map(|(idx, entry)| {
1620                let relations: Vec<_> = entry.relations().collect();
1621                let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1622                if names == vec![package] {
1623                    Some(idx)
1624                } else {
1625                    None
1626                }
1627            })
1628            .collect();
1629
1630        let found = !indices_to_remove.is_empty();
1631
1632        // Remove in reverse order to maintain correct indices
1633        for idx in indices_to_remove.into_iter().rev() {
1634            self.remove_entry(idx);
1635        }
1636
1637        found
1638    }
1639
1640    /// Add a dependency at a specific position or auto-detect the position.
1641    ///
1642    /// If `position` is `None`, the position is automatically determined based
1643    /// on the detected sorting order. If a sorting order is detected, the entry
1644    /// is inserted in the appropriate position to maintain that order. Otherwise,
1645    /// it is appended at the end.
1646    ///
1647    /// # Arguments
1648    /// * `entry` - The entry to add
1649    /// * `position` - Optional position to insert at
1650    ///
1651    /// # Example
1652    /// ```
1653    /// use debian_control::lossless::relations::{Relations, Relation, Entry};
1654    ///
1655    /// let mut relations: Relations = "python3, rustc".parse().unwrap();
1656    /// let entry = Entry::from(Relation::simple("debhelper"));
1657    /// relations.add_dependency(entry, None);
1658    /// // debhelper is inserted in sorted order (if order is detected)
1659    /// ```
1660    pub fn add_dependency(&mut self, entry: Entry, position: Option<usize>) {
1661        let pos = position.unwrap_or_else(|| self.find_insert_position(&entry));
1662        self.insert(pos, entry);
1663    }
1664
1665    /// Get the entry containing a specific package.
1666    ///
1667    /// This returns the first entry that contains exactly one relation with the
1668    /// specified package name (no alternatives).
1669    ///
1670    /// # Arguments
1671    /// * `package` - The package name to search for
1672    ///
1673    /// # Returns
1674    /// A tuple of (index, Entry) if found
1675    ///
1676    /// # Errors
1677    /// Returns `Err` with a message if the package is found in a complex rule (with alternatives)
1678    ///
1679    /// # Example
1680    /// ```
1681    /// use debian_control::lossless::relations::Relations;
1682    ///
1683    /// let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
1684    /// let (idx, entry) = relations.get_relation("debhelper").unwrap();
1685    /// assert_eq!(idx, 1);
1686    /// assert_eq!(entry.to_string(), "debhelper (>= 12)");
1687    /// ```
1688    pub fn get_relation(&self, package: &str) -> Result<(usize, Entry), String> {
1689        for (idx, entry) in self.entries().enumerate() {
1690            let relations: Vec<_> = entry.relations().collect();
1691            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1692
1693            if names.len() > 1 && names.contains(&package.to_string()) {
1694                return Err(format!("Complex rule for {}, aborting", package));
1695            }
1696
1697            if names.len() == 1 && names[0] == package {
1698                return Ok((idx, entry));
1699            }
1700        }
1701        Err(format!("Package {} not found", package))
1702    }
1703
1704    /// Iterate over all entries containing a specific package.
1705    ///
1706    /// # Arguments
1707    /// * `package` - The package name to search for
1708    ///
1709    /// # Returns
1710    /// An iterator over tuples of (index, Entry)
1711    ///
1712    /// # Example
1713    /// ```
1714    /// use debian_control::lossless::relations::Relations;
1715    ///
1716    /// let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
1717    /// let entries: Vec<_> = relations.iter_relations_for("python3").collect();
1718    /// assert_eq!(entries.len(), 1);
1719    /// ```
1720    pub fn iter_relations_for(&self, package: &str) -> impl Iterator<Item = (usize, Entry)> + '_ {
1721        let package = package.to_string();
1722        self.entries().enumerate().filter(move |(_, entry)| {
1723            let names: Vec<_> = entry.relations().map(|r| r.name()).collect();
1724            names.contains(&package)
1725        })
1726    }
1727
1728    /// Check whether a package exists in the relations.
1729    ///
1730    /// # Arguments
1731    /// * `package` - The package name to search for
1732    ///
1733    /// # Returns
1734    /// `true` if the package is found, `false` otherwise
1735    ///
1736    /// # Example
1737    /// ```
1738    /// use debian_control::lossless::relations::Relations;
1739    ///
1740    /// let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1741    /// assert!(relations.has_relation("debhelper"));
1742    /// assert!(!relations.has_relation("nonexistent"));
1743    /// ```
1744    pub fn has_relation(&self, package: &str) -> bool {
1745        self.entries()
1746            .any(|entry| entry.relations().any(|r| r.name() == package))
1747    }
1748}
1749
1750impl From<Vec<Entry>> for Relations {
1751    fn from(entries: Vec<Entry>) -> Self {
1752        let mut builder = GreenNodeBuilder::new();
1753        builder.start_node(ROOT.into());
1754        for (i, entry) in entries.into_iter().enumerate() {
1755            if i > 0 {
1756                builder.token(COMMA.into(), ",");
1757                builder.token(WHITESPACE.into(), " ");
1758            }
1759            inject(&mut builder, entry.0);
1760        }
1761        builder.finish_node();
1762        Relations(SyntaxNode::new_root_mut(builder.finish()))
1763    }
1764}
1765
1766impl From<Entry> for Relations {
1767    fn from(entry: Entry) -> Self {
1768        Self::from(vec![entry])
1769    }
1770}
1771
1772impl Default for Entry {
1773    fn default() -> Self {
1774        Self::new()
1775    }
1776}
1777
1778impl PartialOrd for Entry {
1779    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1780        let mut rels_a = self.relations();
1781        let mut rels_b = other.relations();
1782        while let (Some(a), Some(b)) = (rels_a.next(), rels_b.next()) {
1783            match a.cmp(&b) {
1784                std::cmp::Ordering::Equal => continue,
1785                x => return Some(x),
1786            }
1787        }
1788
1789        if rels_a.next().is_some() {
1790            return Some(std::cmp::Ordering::Greater);
1791        }
1792
1793        if rels_b.next().is_some() {
1794            return Some(std::cmp::Ordering::Less);
1795        }
1796
1797        Some(std::cmp::Ordering::Equal)
1798    }
1799}
1800
1801impl Eq for Entry {}
1802
1803impl Ord for Entry {
1804    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1805        self.partial_cmp(other).unwrap()
1806    }
1807}
1808
1809impl Entry {
1810    /// Create a new entry
1811    pub fn new() -> Self {
1812        let mut builder = GreenNodeBuilder::new();
1813        builder.start_node(SyntaxKind::ENTRY.into());
1814        builder.finish_node();
1815        Entry(SyntaxNode::new_root_mut(builder.finish()))
1816    }
1817
1818    /// Replace the relation at the given index
1819    pub fn replace(&mut self, idx: usize, relation: Relation) {
1820        let current_relation = self.get_relation(idx).unwrap();
1821
1822        let old_root = current_relation.0;
1823        let new_root = relation.0;
1824        // Preserve white the current relation has
1825        let mut prev = new_root.first_child_or_token();
1826        let mut new_head_len = 0;
1827        // First, strip off any whitespace from the new relation
1828        while let Some(p) = prev {
1829            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1830                new_head_len += 1;
1831                prev = p.next_sibling_or_token();
1832            } else {
1833                break;
1834            }
1835        }
1836        let mut new_tail_len = 0;
1837        let mut next = new_root.last_child_or_token();
1838        while let Some(n) = next {
1839            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1840                new_tail_len += 1;
1841                next = n.prev_sibling_or_token();
1842            } else {
1843                break;
1844            }
1845        }
1846        // Then, inherit the whitespace from the old relation
1847        let mut prev = old_root.first_child_or_token();
1848        let mut old_head = vec![];
1849        while let Some(p) = prev {
1850            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1851                old_head.push(p.clone());
1852                prev = p.next_sibling_or_token();
1853            } else {
1854                break;
1855            }
1856        }
1857        let mut old_tail = vec![];
1858        let mut next = old_root.last_child_or_token();
1859        while let Some(n) = next {
1860            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1861                old_tail.push(n.clone());
1862                next = n.prev_sibling_or_token();
1863            } else {
1864                break;
1865            }
1866        }
1867        new_root.splice_children(0..new_head_len, old_head);
1868        let tail_pos = new_root.children_with_tokens().count() - new_tail_len;
1869        new_root.splice_children(
1870            tail_pos - new_tail_len..tail_pos,
1871            old_tail.into_iter().rev(),
1872        );
1873        let index = old_root.index();
1874        self.0
1875            .splice_children(index..index + 1, vec![new_root.into()]);
1876    }
1877
1878    /// Wrap and sort the relations in this entry
1879    #[must_use]
1880    pub fn wrap_and_sort(&self) -> Self {
1881        let mut relations = self
1882            .relations()
1883            .map(|r| r.wrap_and_sort())
1884            .collect::<Vec<_>>();
1885        // TODO: preserve comments
1886        relations.sort();
1887        Self::from(relations)
1888    }
1889
1890    /// Iterate over the relations in this entry
1891    pub fn relations(&self) -> impl Iterator<Item = Relation> + '_ {
1892        self.0.children().filter_map(Relation::cast)
1893    }
1894
1895    /// Iterate over the relations in this entry
1896    pub fn iter(&self) -> impl Iterator<Item = Relation> + '_ {
1897        self.relations()
1898    }
1899
1900    /// Get the relation at the given index
1901    pub fn get_relation(&self, idx: usize) -> Option<Relation> {
1902        self.relations().nth(idx)
1903    }
1904
1905    /// Remove the relation at the given index
1906    ///
1907    /// # Arguments
1908    /// * `idx` - The index of the relation to remove
1909    ///
1910    /// # Example
1911    /// ```
1912    /// use debian_control::lossless::relations::{Relation,Entry};
1913    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-requests".parse().unwrap();
1914    /// entry.remove_relation(1);
1915    /// assert_eq!(entry.to_string(), "python3-dulwich (>= 0.19.0)");
1916    /// ```
1917    pub fn remove_relation(&self, idx: usize) -> Relation {
1918        let mut relation = self.get_relation(idx).unwrap();
1919        relation.remove();
1920        relation
1921    }
1922
1923    /// Check if this entry is satisfied by the given package versions.
1924    ///
1925    /// # Arguments
1926    /// * `package_version` - A function that returns the version of a package.
1927    ///
1928    /// # Example
1929    /// ```
1930    /// use debian_control::lossless::relations::{Relation,Entry};
1931    /// let entry = Entry::from(vec!["samba (>= 2.0)".parse::<Relation>().unwrap()]);
1932    /// assert!(entry.satisfied_by(|name: &str| -> Option<debversion::Version> {
1933    ///    match name {
1934    ///    "samba" => Some("2.0".parse().unwrap()),
1935    ///    _ => None
1936    /// }}));
1937    /// ```
1938    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
1939        self.relations().any(|r| {
1940            let actual = package_version.lookup_version(r.name().as_str());
1941            if let Some((vc, version)) = r.version() {
1942                if let Some(actual) = actual {
1943                    match vc {
1944                        VersionConstraint::GreaterThanEqual => *actual >= version,
1945                        VersionConstraint::LessThanEqual => *actual <= version,
1946                        VersionConstraint::Equal => *actual == version,
1947                        VersionConstraint::GreaterThan => *actual > version,
1948                        VersionConstraint::LessThan => *actual < version,
1949                    }
1950                } else {
1951                    false
1952                }
1953            } else {
1954                actual.is_some()
1955            }
1956        })
1957    }
1958
1959    /// Remove this entry
1960    ///
1961    /// # Example
1962    /// ```
1963    /// use debian_control::lossless::relations::{Relations,Entry};
1964    /// let mut relations: Relations = r"python3-dulwich (>= 0.19.0), python3-urllib3 (<< 1.26.0)".parse().unwrap();
1965    /// let mut entry = relations.get_entry(0).unwrap();
1966    /// entry.remove();
1967    /// assert_eq!(relations.to_string(), "python3-urllib3 (<< 1.26.0)");
1968    /// ```
1969    pub fn remove(&mut self) {
1970        let mut removed_comma = false;
1971        let is_first = !self
1972            .0
1973            .siblings(Direction::Prev)
1974            .skip(1)
1975            .any(|n| n.kind() == ENTRY);
1976        while let Some(n) = self.0.next_sibling_or_token() {
1977            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1978                n.detach();
1979            } else if n.kind() == COMMA {
1980                n.detach();
1981                removed_comma = true;
1982                break;
1983            } else {
1984                panic!("Unexpected node: {:?}", n);
1985            }
1986        }
1987        if !is_first {
1988            while let Some(n) = self.0.prev_sibling_or_token() {
1989                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1990                    n.detach();
1991                } else if !removed_comma && n.kind() == COMMA {
1992                    n.detach();
1993                    break;
1994                } else {
1995                    break;
1996                }
1997            }
1998        } else {
1999            while let Some(n) = self.0.next_sibling_or_token() {
2000                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2001                    n.detach();
2002                } else {
2003                    break;
2004                }
2005            }
2006        }
2007        self.0.detach();
2008    }
2009
2010    /// Check if this entry is empty
2011    pub fn is_empty(&self) -> bool {
2012        self.relations().count() == 0
2013    }
2014
2015    /// Get the number of relations in this entry
2016    pub fn len(&self) -> usize {
2017        self.relations().count()
2018    }
2019
2020    /// Push a new relation to the entry
2021    ///
2022    /// # Arguments
2023    /// * `relation` - The relation to push
2024    ///
2025    /// # Example
2026    /// ```
2027    /// use debian_control::lossless::relations::{Relation,Entry};
2028    /// let mut entry: Entry = "samba (>= 2.0)".parse().unwrap();
2029    /// entry.push("python3-requests".parse().unwrap());
2030    /// assert_eq!(entry.to_string(), "samba (>= 2.0) | python3-requests");
2031    /// ```
2032    pub fn push(&mut self, relation: Relation) {
2033        let is_empty = !self
2034            .0
2035            .children_with_tokens()
2036            .any(|n| n.kind() == PIPE || n.kind() == RELATION);
2037
2038        let (position, new_children) = if let Some(current_relation) = self.relations().last() {
2039            let to_insert: Vec<NodeOrToken<GreenNode, GreenToken>> = if is_empty {
2040                vec![relation.0.green().into()]
2041            } else {
2042                vec![
2043                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
2044                    NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
2045                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
2046                    relation.0.green().into(),
2047                ]
2048            };
2049
2050            (current_relation.0.index() + 1, to_insert)
2051        } else {
2052            let child_count = self.0.children_with_tokens().count();
2053            (
2054                child_count,
2055                if is_empty {
2056                    vec![relation.0.green().into()]
2057                } else {
2058                    vec![
2059                        NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
2060                        NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
2061                        relation.0.green().into(),
2062                    ]
2063                },
2064            )
2065        };
2066
2067        let new_root = SyntaxNode::new_root_mut(
2068            self.0.replace_with(
2069                self.0
2070                    .green()
2071                    .splice_children(position..position, new_children),
2072            ),
2073        );
2074
2075        if let Some(parent) = self.0.parent() {
2076            parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2077            self.0 = parent
2078                .children_with_tokens()
2079                .nth(self.0.index())
2080                .unwrap()
2081                .clone()
2082                .into_node()
2083                .unwrap();
2084        } else {
2085            self.0 = new_root;
2086        }
2087    }
2088
2089    /// Check if this entry (OR-group) is implied by another entry.
2090    ///
2091    /// An entry is implied by another if any of the relations in this entry
2092    /// is implied by any relation in the outer entry. This follows the semantics
2093    /// of OR-groups in Debian dependencies.
2094    ///
2095    /// For example:
2096    /// - `pkg >= 1.0` is implied by `pkg >= 1.5 | libc6` (first relation matches)
2097    /// - `pkg1 | pkg2` is implied by `pkg1` (pkg1 satisfies the requirement)
2098    ///
2099    /// # Arguments
2100    /// * `outer` - The outer entry that may imply this entry
2101    ///
2102    /// # Returns
2103    /// `true` if this entry is implied by `outer`, `false` otherwise
2104    ///
2105    /// # Example
2106    /// ```
2107    /// use debian_control::lossless::relations::Entry;
2108    ///
2109    /// let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
2110    /// let outer: Entry = "pkg (>= 1.5) | libc6".parse().unwrap();
2111    /// assert!(inner.is_implied_by(&outer));
2112    /// ```
2113    pub fn is_implied_by(&self, outer: &Entry) -> bool {
2114        // If entries are identical, they imply each other
2115        if self == outer {
2116            return true;
2117        }
2118
2119        // Check if any relation in inner is implied by any relation in outer
2120        for inner_rel in self.relations() {
2121            for outer_rel in outer.relations() {
2122                if inner_rel.is_implied_by(&outer_rel) {
2123                    return true;
2124                }
2125            }
2126        }
2127
2128        false
2129    }
2130}
2131
2132fn inject(builder: &mut GreenNodeBuilder, node: SyntaxNode) {
2133    builder.start_node(node.kind().into());
2134    for child in node.children_with_tokens() {
2135        match child {
2136            rowan::NodeOrToken::Node(child) => {
2137                inject(builder, child);
2138            }
2139            rowan::NodeOrToken::Token(token) => {
2140                builder.token(token.kind().into(), token.text());
2141            }
2142        }
2143    }
2144    builder.finish_node();
2145}
2146
2147impl From<Vec<Relation>> for Entry {
2148    fn from(relations: Vec<Relation>) -> Self {
2149        let mut builder = GreenNodeBuilder::new();
2150        builder.start_node(SyntaxKind::ENTRY.into());
2151        for (i, relation) in relations.into_iter().enumerate() {
2152            if i > 0 {
2153                builder.token(WHITESPACE.into(), " ");
2154                builder.token(COMMA.into(), "|");
2155                builder.token(WHITESPACE.into(), " ");
2156            }
2157            inject(&mut builder, relation.0);
2158        }
2159        builder.finish_node();
2160        Entry(SyntaxNode::new_root_mut(builder.finish()))
2161    }
2162}
2163
2164impl From<Relation> for Entry {
2165    fn from(relation: Relation) -> Self {
2166        Self::from(vec![relation])
2167    }
2168}
2169
2170/// Helper function to tokenize a version string, handling epochs
2171/// Version strings like "1:2.3.2-2~" need to be split into: IDENT("1"), COLON, IDENT("2.3.2-2~")
2172fn tokenize_version(builder: &mut GreenNodeBuilder, version: &Version) {
2173    let version_str = version.to_string();
2174
2175    // Split on the first colon (if any) to handle epochs
2176    if let Some(colon_pos) = version_str.find(':') {
2177        // Epoch part (before colon)
2178        builder.token(IDENT.into(), &version_str[..colon_pos]);
2179        builder.token(COLON.into(), ":");
2180        // Version part (after colon)
2181        builder.token(IDENT.into(), &version_str[colon_pos + 1..]);
2182    } else {
2183        // No epoch, just a regular version
2184        builder.token(IDENT.into(), version_str.as_str());
2185    }
2186}
2187
2188impl Relation {
2189    /// Create a new relation
2190    ///
2191    /// # Arguments
2192    /// * `name` - The name of the package
2193    /// * `version_constraint` - The version constraint and version to use
2194    ///
2195    /// # Example
2196    /// ```
2197    /// use debian_control::lossless::relations::{Relation};
2198    /// use debian_control::relations::VersionConstraint;
2199    /// let relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2200    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2201    /// ```
2202    pub fn new(name: &str, version_constraint: Option<(VersionConstraint, Version)>) -> Self {
2203        let mut builder = GreenNodeBuilder::new();
2204        builder.start_node(SyntaxKind::RELATION.into());
2205        builder.token(IDENT.into(), name);
2206        if let Some((vc, version)) = version_constraint {
2207            builder.token(WHITESPACE.into(), " ");
2208            builder.start_node(SyntaxKind::VERSION.into());
2209            builder.token(L_PARENS.into(), "(");
2210            builder.start_node(SyntaxKind::CONSTRAINT.into());
2211            for c in vc.to_string().chars() {
2212                builder.token(
2213                    match c {
2214                        '>' => R_ANGLE.into(),
2215                        '<' => L_ANGLE.into(),
2216                        '=' => EQUAL.into(),
2217                        _ => unreachable!(),
2218                    },
2219                    c.to_string().as_str(),
2220                );
2221            }
2222            builder.finish_node();
2223
2224            builder.token(WHITESPACE.into(), " ");
2225
2226            tokenize_version(&mut builder, &version);
2227
2228            builder.token(R_PARENS.into(), ")");
2229
2230            builder.finish_node();
2231        }
2232
2233        builder.finish_node();
2234        Relation(SyntaxNode::new_root_mut(builder.finish()))
2235    }
2236
2237    /// Wrap and sort this relation
2238    ///
2239    /// # Example
2240    /// ```
2241    /// use debian_control::lossless::relations::Relation;
2242    /// let relation = "  samba  (  >= 2.0) ".parse::<Relation>().unwrap();
2243    /// assert_eq!(relation.wrap_and_sort().to_string(), "samba (>= 2.0)");
2244    /// ```
2245    #[must_use]
2246    pub fn wrap_and_sort(&self) -> Self {
2247        let mut builder = GreenNodeBuilder::new();
2248        builder.start_node(SyntaxKind::RELATION.into());
2249        builder.token(IDENT.into(), self.name().as_str());
2250        if let Some(archqual) = self.archqual() {
2251            builder.token(COLON.into(), ":");
2252            builder.token(IDENT.into(), archqual.as_str());
2253        }
2254        if let Some((vc, version)) = self.version() {
2255            builder.token(WHITESPACE.into(), " ");
2256            builder.start_node(SyntaxKind::VERSION.into());
2257            builder.token(L_PARENS.into(), "(");
2258            builder.start_node(SyntaxKind::CONSTRAINT.into());
2259            builder.token(
2260                match vc {
2261                    VersionConstraint::GreaterThanEqual => R_ANGLE.into(),
2262                    VersionConstraint::LessThanEqual => L_ANGLE.into(),
2263                    VersionConstraint::Equal => EQUAL.into(),
2264                    VersionConstraint::GreaterThan => R_ANGLE.into(),
2265                    VersionConstraint::LessThan => L_ANGLE.into(),
2266                },
2267                vc.to_string().as_str(),
2268            );
2269            builder.finish_node();
2270            builder.token(WHITESPACE.into(), " ");
2271            tokenize_version(&mut builder, &version);
2272            builder.token(R_PARENS.into(), ")");
2273            builder.finish_node();
2274        }
2275        if let Some(architectures) = self.architectures() {
2276            builder.token(WHITESPACE.into(), " ");
2277            builder.start_node(ARCHITECTURES.into());
2278            builder.token(L_BRACKET.into(), "[");
2279            for (i, arch) in architectures.enumerate() {
2280                if i > 0 {
2281                    builder.token(WHITESPACE.into(), " ");
2282                }
2283                builder.token(IDENT.into(), arch.as_str());
2284            }
2285            builder.token(R_BRACKET.into(), "]");
2286            builder.finish_node();
2287        }
2288        for profiles in self.profiles() {
2289            builder.token(WHITESPACE.into(), " ");
2290            builder.start_node(PROFILES.into());
2291            builder.token(L_ANGLE.into(), "<");
2292            for (i, profile) in profiles.into_iter().enumerate() {
2293                if i > 0 {
2294                    builder.token(WHITESPACE.into(), " ");
2295                }
2296                match profile {
2297                    BuildProfile::Disabled(name) => {
2298                        builder.token(NOT.into(), "!");
2299                        builder.token(IDENT.into(), name.as_str());
2300                    }
2301                    BuildProfile::Enabled(name) => {
2302                        builder.token(IDENT.into(), name.as_str());
2303                    }
2304                }
2305            }
2306            builder.token(R_ANGLE.into(), ">");
2307            builder.finish_node();
2308        }
2309        builder.finish_node();
2310        Relation(SyntaxNode::new_root_mut(builder.finish()))
2311    }
2312
2313    /// Create a new simple relation, without any version constraints.
2314    ///
2315    /// # Example
2316    /// ```
2317    /// use debian_control::lossless::relations::Relation;
2318    /// let relation = Relation::simple("samba");
2319    /// assert_eq!(relation.to_string(), "samba");
2320    /// ```
2321    pub fn simple(name: &str) -> Self {
2322        Self::new(name, None)
2323    }
2324
2325    /// Remove the version constraint from the relation.
2326    ///
2327    /// # Example
2328    /// ```
2329    /// use debian_control::lossless::relations::{Relation};
2330    /// use debian_control::relations::VersionConstraint;
2331    /// let mut relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2332    /// relation.drop_constraint();
2333    /// assert_eq!(relation.to_string(), "samba");
2334    /// ```
2335    pub fn drop_constraint(&mut self) -> bool {
2336        let version_token = self.0.children().find(|n| n.kind() == VERSION);
2337        if let Some(version_token) = version_token {
2338            // Remove any whitespace before the version token
2339            while let Some(prev) = version_token.prev_sibling_or_token() {
2340                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2341                    prev.detach();
2342                } else {
2343                    break;
2344                }
2345            }
2346            version_token.detach();
2347            return true;
2348        }
2349
2350        false
2351    }
2352
2353    /// Return the name of the package in the relation.
2354    ///
2355    /// # Example
2356    /// ```
2357    /// use debian_control::lossless::relations::Relation;
2358    /// let relation = Relation::simple("samba");
2359    /// assert_eq!(relation.name(), "samba");
2360    /// ```
2361    pub fn name(&self) -> String {
2362        self.0
2363            .children_with_tokens()
2364            .find_map(|it| match it {
2365                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2366                _ => None,
2367            })
2368            .unwrap()
2369            .text()
2370            .to_string()
2371    }
2372
2373    /// Return the archqual
2374    ///
2375    /// # Example
2376    /// ```
2377    /// use debian_control::lossless::relations::Relation;
2378    /// let relation: Relation = "samba:any".parse().unwrap();
2379    /// assert_eq!(relation.archqual(), Some("any".to_string()));
2380    /// ```
2381    pub fn archqual(&self) -> Option<String> {
2382        let archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2383        let node = if let Some(archqual) = archqual {
2384            archqual.children_with_tokens().find_map(|it| match it {
2385                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2386                _ => None,
2387            })
2388        } else {
2389            None
2390        };
2391        node.map(|n| n.text().to_string())
2392    }
2393
2394    /// Set the architecture qualifier for this relation.
2395    ///
2396    /// # Example
2397    /// ```
2398    /// use debian_control::lossless::relations::Relation;
2399    /// let mut relation = Relation::simple("samba");
2400    /// relation.set_archqual("any");
2401    /// assert_eq!(relation.to_string(), "samba:any");
2402    /// ```
2403    pub fn set_archqual(&mut self, archqual: &str) {
2404        let mut builder = GreenNodeBuilder::new();
2405        builder.start_node(ARCHQUAL.into());
2406        builder.token(COLON.into(), ":");
2407        builder.token(IDENT.into(), archqual);
2408        builder.finish_node();
2409
2410        let node_archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2411        if let Some(node_archqual) = node_archqual {
2412            self.0.splice_children(
2413                node_archqual.index()..node_archqual.index() + 1,
2414                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2415            );
2416        } else {
2417            let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2418            let idx = if let Some(name_node) = name_node {
2419                name_node.index() + 1
2420            } else {
2421                0
2422            };
2423            self.0.splice_children(
2424                idx..idx,
2425                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2426            );
2427        }
2428    }
2429
2430    /// Return the version constraint and the version it is constrained to.
2431    pub fn version(&self) -> Option<(VersionConstraint, Version)> {
2432        let vc = self.0.children().find(|n| n.kind() == VERSION);
2433        let vc = vc.as_ref()?;
2434        let constraint = vc.children().find(|n| n.kind() == CONSTRAINT);
2435
2436        // Collect all IDENT and COLON tokens to handle versions with epochs (e.g., "1:2.3.2-2~")
2437        let version_str: String = vc
2438            .children_with_tokens()
2439            .filter_map(|it| match it {
2440                SyntaxElement::Token(token) if token.kind() == IDENT || token.kind() == COLON => {
2441                    Some(token.text().to_string())
2442                }
2443                _ => None,
2444            })
2445            .collect();
2446
2447        if let Some(constraint) = constraint {
2448            if !version_str.is_empty() {
2449                let vc: VersionConstraint = constraint.to_string().parse().unwrap();
2450                Some((vc, version_str.parse().unwrap()))
2451            } else {
2452                None
2453            }
2454        } else {
2455            None
2456        }
2457    }
2458
2459    /// Set the version constraint for this relation
2460    ///
2461    /// # Example
2462    /// ```
2463    /// use debian_control::lossless::relations::{Relation};
2464    /// use debian_control::relations::VersionConstraint;
2465    /// let mut relation = Relation::simple("samba");
2466    /// relation.set_version(Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2467    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2468    /// ```
2469    pub fn set_version(&mut self, version_constraint: Option<(VersionConstraint, Version)>) {
2470        let current_version = self.0.children().find(|n| n.kind() == VERSION);
2471        if let Some((vc, version)) = version_constraint {
2472            let mut builder = GreenNodeBuilder::new();
2473            builder.start_node(VERSION.into());
2474            builder.token(L_PARENS.into(), "(");
2475            builder.start_node(CONSTRAINT.into());
2476            match vc {
2477                VersionConstraint::GreaterThanEqual => {
2478                    builder.token(R_ANGLE.into(), ">");
2479                    builder.token(EQUAL.into(), "=");
2480                }
2481                VersionConstraint::LessThanEqual => {
2482                    builder.token(L_ANGLE.into(), "<");
2483                    builder.token(EQUAL.into(), "=");
2484                }
2485                VersionConstraint::Equal => {
2486                    builder.token(EQUAL.into(), "=");
2487                }
2488                VersionConstraint::GreaterThan => {
2489                    builder.token(R_ANGLE.into(), ">");
2490                }
2491                VersionConstraint::LessThan => {
2492                    builder.token(L_ANGLE.into(), "<");
2493                }
2494            }
2495            builder.finish_node(); // CONSTRAINT
2496            builder.token(WHITESPACE.into(), " ");
2497            tokenize_version(&mut builder, &version);
2498            builder.token(R_PARENS.into(), ")");
2499            builder.finish_node(); // VERSION
2500
2501            if let Some(current_version) = current_version {
2502                self.0.splice_children(
2503                    current_version.index()..current_version.index() + 1,
2504                    vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2505                );
2506            } else {
2507                let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2508                let idx = if let Some(name_node) = name_node {
2509                    name_node.index() + 1
2510                } else {
2511                    0
2512                };
2513                let new_children = vec![
2514                    GreenToken::new(WHITESPACE.into(), " ").into(),
2515                    builder.finish().into(),
2516                ];
2517                let new_root = SyntaxNode::new_root_mut(
2518                    self.0.green().splice_children(idx..idx, new_children),
2519                );
2520                if let Some(parent) = self.0.parent() {
2521                    parent
2522                        .splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2523                    self.0 = parent
2524                        .children_with_tokens()
2525                        .nth(self.0.index())
2526                        .unwrap()
2527                        .clone()
2528                        .into_node()
2529                        .unwrap();
2530                } else {
2531                    self.0 = new_root;
2532                }
2533            }
2534        } else if let Some(current_version) = current_version {
2535            // Remove any whitespace before the version token
2536            while let Some(prev) = current_version.prev_sibling_or_token() {
2537                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2538                    prev.detach();
2539                } else {
2540                    break;
2541                }
2542            }
2543            current_version.detach();
2544        }
2545    }
2546
2547    /// Return an iterator over the architectures for this relation
2548    ///
2549    /// # Example
2550    /// ```
2551    /// use debian_control::lossless::relations::Relation;
2552    /// let relation: Relation = "samba [amd64]".parse().unwrap();
2553    /// assert_eq!(relation.architectures().unwrap().collect::<Vec<_>>(), vec!["amd64".to_string()]);
2554    /// ```
2555    pub fn architectures(&self) -> Option<impl Iterator<Item = String> + '_> {
2556        let architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES)?;
2557
2558        Some(architectures.children_with_tokens().filter_map(|node| {
2559            let token = node.as_token()?;
2560            if token.kind() == IDENT {
2561                Some(token.text().to_string())
2562            } else {
2563                None
2564            }
2565        }))
2566    }
2567
2568    /// Returns an iterator over the build profiles for this relation
2569    ///
2570    /// # Example
2571    /// ```
2572    /// use debian_control::lossless::relations::{Relation};
2573    /// use debian_control::relations::{BuildProfile};
2574    /// let relation: Relation = "samba <!nocheck>".parse().unwrap();
2575    /// assert_eq!(relation.profiles().collect::<Vec<_>>(), vec![vec![BuildProfile::Disabled("nocheck".to_string())]]);
2576    /// ```
2577    pub fn profiles(&self) -> impl Iterator<Item = Vec<BuildProfile>> + '_ {
2578        let profiles = self.0.children().filter(|n| n.kind() == PROFILES);
2579
2580        profiles.map(|profile| {
2581            // iterate over nodes separated by whitespace tokens
2582            let mut ret = vec![];
2583            let mut current = vec![];
2584            for token in profile.children_with_tokens() {
2585                match token.kind() {
2586                    WHITESPACE | NEWLINE => {
2587                        if !current.is_empty() {
2588                            ret.push(current.join("").parse::<BuildProfile>().unwrap());
2589                            current = vec![];
2590                        }
2591                    }
2592                    L_ANGLE | R_ANGLE => {}
2593                    _ => {
2594                        current.push(token.to_string());
2595                    }
2596                }
2597            }
2598            if !current.is_empty() {
2599                ret.push(current.concat().parse().unwrap());
2600            }
2601            ret
2602        })
2603    }
2604
2605    /// Remove this relation
2606    ///
2607    /// # Example
2608    /// ```
2609    /// use debian_control::lossless::relations::{Relation,Entry};
2610    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-urllib3 (<< 1.26.0)".parse().unwrap();
2611    /// let mut relation = entry.get_relation(0).unwrap();
2612    /// relation.remove();
2613    /// assert_eq!(entry.to_string(), "python3-urllib3 (<< 1.26.0)");
2614    /// ```
2615    pub fn remove(&mut self) {
2616        let is_first = !self
2617            .0
2618            .siblings(Direction::Prev)
2619            .skip(1)
2620            .any(|n| n.kind() == RELATION);
2621        if !is_first {
2622            // Not the first item in the list. Remove whitespace backwards to the previous
2623            // pipe, the pipe and any whitespace until the previous relation
2624            while let Some(n) = self.0.prev_sibling_or_token() {
2625                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2626                    n.detach();
2627                } else if n.kind() == PIPE {
2628                    n.detach();
2629                    break;
2630                } else {
2631                    break;
2632                }
2633            }
2634            while let Some(n) = self.0.prev_sibling_or_token() {
2635                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2636                    n.detach();
2637                } else {
2638                    break;
2639                }
2640            }
2641        } else {
2642            // First item in the list. Remove whitespace up to the pipe, the pipe and anything
2643            // before the next relation
2644            while let Some(n) = self.0.next_sibling_or_token() {
2645                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2646                    n.detach();
2647                } else if n.kind() == PIPE {
2648                    n.detach();
2649                    break;
2650                } else {
2651                    panic!("Unexpected node: {:?}", n);
2652                }
2653            }
2654
2655            while let Some(n) = self.0.next_sibling_or_token() {
2656                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2657                    n.detach();
2658                } else {
2659                    break;
2660                }
2661            }
2662        }
2663        // If this was the last relation in the entry, remove the entire entry
2664        if let Some(mut parent) = self.0.parent().and_then(Entry::cast) {
2665            if parent.is_empty() {
2666                parent.remove();
2667            } else {
2668                self.0.detach();
2669            }
2670        } else {
2671            self.0.detach();
2672        }
2673    }
2674
2675    /// Set the architectures for this relation
2676    ///
2677    /// # Example
2678    /// ```
2679    /// use debian_control::lossless::relations::Relation;
2680    /// let mut relation = Relation::simple("samba");
2681    /// relation.set_architectures(vec!["amd64", "i386"].into_iter());
2682    /// assert_eq!(relation.to_string(), "samba [amd64 i386]");
2683    /// ```
2684    pub fn set_architectures<'a>(&mut self, architectures: impl Iterator<Item = &'a str>) {
2685        let mut builder = GreenNodeBuilder::new();
2686        builder.start_node(ARCHITECTURES.into());
2687        builder.token(L_BRACKET.into(), "[");
2688        for (i, arch) in architectures.enumerate() {
2689            if i > 0 {
2690                builder.token(WHITESPACE.into(), " ");
2691            }
2692            builder.token(IDENT.into(), arch);
2693        }
2694        builder.token(R_BRACKET.into(), "]");
2695        builder.finish_node();
2696
2697        let node_architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES);
2698        if let Some(node_architectures) = node_architectures {
2699            let new_root = SyntaxNode::new_root_mut(builder.finish());
2700            self.0.splice_children(
2701                node_architectures.index()..node_architectures.index() + 1,
2702                vec![new_root.into()],
2703            );
2704        } else {
2705            let profiles = self.0.children().find(|n| n.kind() == PROFILES);
2706            let idx = if let Some(profiles) = profiles {
2707                profiles.index()
2708            } else {
2709                self.0.children_with_tokens().count()
2710            };
2711            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2712                idx..idx,
2713                vec![
2714                    GreenToken::new(WHITESPACE.into(), " ").into(),
2715                    builder.finish().into(),
2716                ],
2717            ));
2718            if let Some(parent) = self.0.parent() {
2719                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2720                self.0 = parent
2721                    .children_with_tokens()
2722                    .nth(self.0.index())
2723                    .unwrap()
2724                    .clone()
2725                    .into_node()
2726                    .unwrap();
2727            } else {
2728                self.0 = new_root;
2729            }
2730        }
2731    }
2732
2733    /// Add a build profile to this relation
2734    ///
2735    /// # Example
2736    /// ```
2737    /// use debian_control::lossless::relations::Relation;
2738    /// use debian_control::relations::BuildProfile;
2739    /// let mut relation = Relation::simple("samba");
2740    /// relation.add_profile(&[BuildProfile::Disabled("nocheck".to_string())]);
2741    /// assert_eq!(relation.to_string(), "samba <!nocheck>");
2742    /// ```
2743    pub fn add_profile(&mut self, profile: &[BuildProfile]) {
2744        let mut builder = GreenNodeBuilder::new();
2745        builder.start_node(PROFILES.into());
2746        builder.token(L_ANGLE.into(), "<");
2747        for (i, profile) in profile.iter().enumerate() {
2748            if i > 0 {
2749                builder.token(WHITESPACE.into(), " ");
2750            }
2751            match profile {
2752                BuildProfile::Disabled(name) => {
2753                    builder.token(NOT.into(), "!");
2754                    builder.token(IDENT.into(), name.as_str());
2755                }
2756                BuildProfile::Enabled(name) => {
2757                    builder.token(IDENT.into(), name.as_str());
2758                }
2759            }
2760        }
2761        builder.token(R_ANGLE.into(), ">");
2762        builder.finish_node();
2763
2764        let node_profiles = self.0.children().find(|n| n.kind() == PROFILES);
2765        if let Some(node_profiles) = node_profiles {
2766            let new_root = SyntaxNode::new_root_mut(builder.finish());
2767            self.0.splice_children(
2768                node_profiles.index()..node_profiles.index() + 1,
2769                vec![new_root.into()],
2770            );
2771        } else {
2772            let idx = self.0.children_with_tokens().count();
2773            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2774                idx..idx,
2775                vec![
2776                    GreenToken::new(WHITESPACE.into(), " ").into(),
2777                    builder.finish().into(),
2778                ],
2779            ));
2780            if let Some(parent) = self.0.parent() {
2781                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2782                self.0 = parent
2783                    .children_with_tokens()
2784                    .nth(self.0.index())
2785                    .unwrap()
2786                    .clone()
2787                    .into_node()
2788                    .unwrap();
2789            } else {
2790                self.0 = new_root;
2791            }
2792        }
2793    }
2794
2795    /// Build a new relation
2796    pub fn build(name: &str) -> RelationBuilder {
2797        RelationBuilder::new(name)
2798    }
2799
2800    /// Check if this relation is implied by another relation.
2801    ///
2802    /// A relation is implied by another if the outer relation is more restrictive
2803    /// or equal to this relation. For example:
2804    /// - `pkg >= 1.0` is implied by `pkg >= 1.5` (outer is more restrictive)
2805    /// - `pkg >= 1.0` is implied by `pkg = 1.5` (outer is more restrictive)
2806    /// - `pkg` (no version) is implied by any versioned constraint on `pkg`
2807    ///
2808    /// # Arguments
2809    /// * `outer` - The outer relation that may imply this relation
2810    ///
2811    /// # Returns
2812    /// `true` if this relation is implied by `outer`, `false` otherwise
2813    ///
2814    /// # Example
2815    /// ```
2816    /// use debian_control::lossless::relations::Relation;
2817    /// use debian_control::relations::VersionConstraint;
2818    ///
2819    /// let inner = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())));
2820    /// let outer = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())));
2821    /// assert!(inner.is_implied_by(&outer));
2822    ///
2823    /// let inner2 = Relation::new("pkg", None);
2824    /// assert!(inner2.is_implied_by(&outer));
2825    /// ```
2826    pub fn is_implied_by(&self, outer: &Relation) -> bool {
2827        if self.name() != outer.name() {
2828            return false;
2829        }
2830
2831        let inner_version = self.version();
2832        let outer_version = outer.version();
2833
2834        // No version constraint on inner means it's always implied
2835        if inner_version.is_none() {
2836            return true;
2837        }
2838
2839        // If versions are identical, they imply each other
2840        if inner_version == outer_version {
2841            return true;
2842        }
2843
2844        // Inner has version but outer doesn't - not implied
2845        if outer_version.is_none() {
2846            return false;
2847        }
2848
2849        let (inner_constraint, inner_ver) = inner_version.unwrap();
2850        let (outer_constraint, outer_ver) = outer_version.unwrap();
2851
2852        use VersionConstraint::*;
2853        match inner_constraint {
2854            GreaterThanEqual => match outer_constraint {
2855                GreaterThan => outer_ver > inner_ver,
2856                GreaterThanEqual | Equal => outer_ver >= inner_ver,
2857                LessThan | LessThanEqual => false,
2858            },
2859            Equal => match outer_constraint {
2860                Equal => outer_ver == inner_ver,
2861                _ => false,
2862            },
2863            LessThan => match outer_constraint {
2864                LessThan => outer_ver <= inner_ver,
2865                LessThanEqual | Equal => outer_ver < inner_ver,
2866                GreaterThan | GreaterThanEqual => false,
2867            },
2868            LessThanEqual => match outer_constraint {
2869                LessThanEqual | Equal | LessThan => outer_ver <= inner_ver,
2870                GreaterThan | GreaterThanEqual => false,
2871            },
2872            GreaterThan => match outer_constraint {
2873                GreaterThan => outer_ver >= inner_ver,
2874                Equal | GreaterThanEqual => outer_ver > inner_ver,
2875                LessThan | LessThanEqual => false,
2876            },
2877        }
2878    }
2879}
2880
2881/// A builder for creating a `Relation`
2882///
2883/// # Example
2884/// ```
2885/// use debian_control::lossless::relations::{Relation};
2886/// use debian_control::relations::VersionConstraint;
2887/// let relation = Relation::build("samba")
2888///    .version_constraint(VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())
2889///    .archqual("any")
2890///    .architectures(vec!["amd64".to_string(), "i386".to_string()])
2891///    .build();
2892/// assert_eq!(relation.to_string(), "samba:any (>= 2.0) [amd64 i386]");
2893/// ```
2894pub struct RelationBuilder {
2895    name: String,
2896    version_constraint: Option<(VersionConstraint, Version)>,
2897    archqual: Option<String>,
2898    architectures: Option<Vec<String>>,
2899    profiles: Vec<Vec<BuildProfile>>,
2900}
2901
2902impl RelationBuilder {
2903    /// Create a new `RelationBuilder` with the given package name
2904    fn new(name: &str) -> Self {
2905        Self {
2906            name: name.to_string(),
2907            version_constraint: None,
2908            archqual: None,
2909            architectures: None,
2910            profiles: vec![],
2911        }
2912    }
2913
2914    /// Set the version constraint for this relation
2915    pub fn version_constraint(mut self, vc: VersionConstraint, version: Version) -> Self {
2916        self.version_constraint = Some((vc, version));
2917        self
2918    }
2919
2920    /// Set the architecture qualifier for this relation
2921    pub fn archqual(mut self, archqual: &str) -> Self {
2922        self.archqual = Some(archqual.to_string());
2923        self
2924    }
2925
2926    /// Set the architectures for this relation
2927    pub fn architectures(mut self, architectures: Vec<String>) -> Self {
2928        self.architectures = Some(architectures);
2929        self
2930    }
2931
2932    /// Set the build profiles for this relation
2933    pub fn profiles(mut self, profiles: Vec<Vec<BuildProfile>>) -> Self {
2934        self.profiles = profiles;
2935        self
2936    }
2937
2938    /// Add a build profile to this relation
2939    pub fn add_profile(mut self, profile: Vec<BuildProfile>) -> Self {
2940        self.profiles.push(profile);
2941        self
2942    }
2943
2944    /// Build the `Relation`
2945    pub fn build(self) -> Relation {
2946        let mut relation = Relation::new(&self.name, self.version_constraint);
2947        if let Some(archqual) = &self.archqual {
2948            relation.set_archqual(archqual);
2949        }
2950        if let Some(architectures) = &self.architectures {
2951            relation.set_architectures(architectures.iter().map(|s| s.as_str()));
2952        }
2953        for profile in &self.profiles {
2954            relation.add_profile(profile);
2955        }
2956        relation
2957    }
2958}
2959
2960impl PartialOrd for Relation {
2961    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2962        // Compare by name first, then by version
2963        let name_cmp = self.name().cmp(&other.name());
2964        if name_cmp != std::cmp::Ordering::Equal {
2965            return Some(name_cmp);
2966        }
2967
2968        let self_version = self.version();
2969        let other_version = other.version();
2970
2971        match (self_version, other_version) {
2972            (Some((self_vc, self_version)), Some((other_vc, other_version))) => {
2973                let vc_cmp = self_vc.cmp(&other_vc);
2974                if vc_cmp != std::cmp::Ordering::Equal {
2975                    return Some(vc_cmp);
2976                }
2977
2978                Some(self_version.cmp(&other_version))
2979            }
2980            (Some(_), None) => Some(std::cmp::Ordering::Greater),
2981            (None, Some(_)) => Some(std::cmp::Ordering::Less),
2982            (None, None) => Some(std::cmp::Ordering::Equal),
2983        }
2984    }
2985}
2986
2987impl Eq for Relation {}
2988
2989impl Ord for Relation {
2990    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2991        self.partial_cmp(other).unwrap()
2992    }
2993}
2994
2995impl std::str::FromStr for Relations {
2996    type Err = String;
2997
2998    fn from_str(s: &str) -> Result<Self, Self::Err> {
2999        let parse = parse(s, false);
3000        if parse.errors.is_empty() {
3001            Ok(parse.root_mut())
3002        } else {
3003            Err(parse.errors.join("\n"))
3004        }
3005    }
3006}
3007
3008impl std::str::FromStr for Entry {
3009    type Err = String;
3010
3011    fn from_str(s: &str) -> Result<Self, Self::Err> {
3012        let root: Relations = s.parse()?;
3013
3014        let mut entries = root.entries();
3015        let entry = if let Some(entry) = entries.next() {
3016            entry
3017        } else {
3018            return Err("No entry found".to_string());
3019        };
3020
3021        if entries.next().is_some() {
3022            return Err("Multiple entries found".to_string());
3023        }
3024
3025        Ok(entry)
3026    }
3027}
3028
3029impl std::str::FromStr for Relation {
3030    type Err = String;
3031
3032    fn from_str(s: &str) -> Result<Self, Self::Err> {
3033        let entry: Entry = s.parse()?;
3034
3035        let mut relations = entry.relations();
3036        let relation = if let Some(relation) = relations.next() {
3037            relation
3038        } else {
3039            return Err("No relation found".to_string());
3040        };
3041
3042        if relations.next().is_some() {
3043            return Err("Multiple relations found".to_string());
3044        }
3045
3046        Ok(relation)
3047    }
3048}
3049
3050impl From<crate::lossy::Relation> for Relation {
3051    fn from(relation: crate::lossy::Relation) -> Self {
3052        let mut builder = Relation::build(&relation.name);
3053
3054        if let Some((vc, version)) = relation.version {
3055            builder = builder.version_constraint(vc, version);
3056        }
3057
3058        if let Some(archqual) = relation.archqual {
3059            builder = builder.archqual(&archqual);
3060        }
3061
3062        if let Some(architectures) = relation.architectures {
3063            builder = builder.architectures(architectures);
3064        }
3065
3066        builder = builder.profiles(relation.profiles);
3067
3068        builder.build()
3069    }
3070}
3071
3072impl From<Relation> for crate::lossy::Relation {
3073    fn from(relation: Relation) -> Self {
3074        crate::lossy::Relation {
3075            name: relation.name(),
3076            version: relation.version(),
3077            archqual: relation.archqual(),
3078            architectures: relation.architectures().map(|a| a.collect()),
3079            profiles: relation.profiles().collect(),
3080        }
3081    }
3082}
3083
3084impl From<Entry> for Vec<crate::lossy::Relation> {
3085    fn from(entry: Entry) -> Self {
3086        entry.relations().map(|r| r.into()).collect()
3087    }
3088}
3089
3090impl From<Vec<crate::lossy::Relation>> for Entry {
3091    fn from(relations: Vec<crate::lossy::Relation>) -> Self {
3092        let relations: Vec<Relation> = relations.into_iter().map(|r| r.into()).collect();
3093        Entry::from(relations)
3094    }
3095}
3096
3097#[cfg(test)]
3098mod tests {
3099    use super::*;
3100
3101    #[test]
3102    fn test_parse() {
3103        let input = "python3-dulwich";
3104        let parsed: Relations = input.parse().unwrap();
3105        assert_eq!(parsed.to_string(), input);
3106        assert_eq!(parsed.entries().count(), 1);
3107        let entry = parsed.entries().next().unwrap();
3108        assert_eq!(entry.to_string(), "python3-dulwich");
3109        assert_eq!(entry.relations().count(), 1);
3110        let relation = entry.relations().next().unwrap();
3111        assert_eq!(relation.to_string(), "python3-dulwich");
3112        assert_eq!(relation.version(), None);
3113
3114        let input = "python3-dulwich (>= 0.20.21)";
3115        let parsed: Relations = input.parse().unwrap();
3116        assert_eq!(parsed.to_string(), input);
3117        assert_eq!(parsed.entries().count(), 1);
3118        let entry = parsed.entries().next().unwrap();
3119        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3120        assert_eq!(entry.relations().count(), 1);
3121        let relation = entry.relations().next().unwrap();
3122        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3123        assert_eq!(
3124            relation.version(),
3125            Some((
3126                VersionConstraint::GreaterThanEqual,
3127                "0.20.21".parse().unwrap()
3128            ))
3129        );
3130    }
3131
3132    #[test]
3133    fn test_multiple() {
3134        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
3135        let parsed: Relations = input.parse().unwrap();
3136        assert_eq!(parsed.to_string(), input);
3137        assert_eq!(parsed.entries().count(), 2);
3138        let entry = parsed.entries().next().unwrap();
3139        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3140        assert_eq!(entry.relations().count(), 1);
3141        let relation = entry.relations().next().unwrap();
3142        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3143        assert_eq!(
3144            relation.version(),
3145            Some((
3146                VersionConstraint::GreaterThanEqual,
3147                "0.20.21".parse().unwrap()
3148            ))
3149        );
3150        let entry = parsed.entries().nth(1).unwrap();
3151        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.21)");
3152        assert_eq!(entry.relations().count(), 1);
3153        let relation = entry.relations().next().unwrap();
3154        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
3155        assert_eq!(
3156            relation.version(),
3157            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
3158        );
3159    }
3160
3161    #[test]
3162    fn test_architectures() {
3163        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
3164        let parsed: Relations = input.parse().unwrap();
3165        assert_eq!(parsed.to_string(), input);
3166        assert_eq!(parsed.entries().count(), 1);
3167        let entry = parsed.entries().next().unwrap();
3168        assert_eq!(
3169            entry.to_string(),
3170            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3171        );
3172        assert_eq!(entry.relations().count(), 1);
3173        let relation = entry.relations().next().unwrap();
3174        assert_eq!(
3175            relation.to_string(),
3176            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3177        );
3178        assert_eq!(relation.version(), None);
3179        assert_eq!(
3180            relation.architectures().unwrap().collect::<Vec<_>>(),
3181            vec![
3182                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
3183            ]
3184            .into_iter()
3185            .map(|s| s.to_string())
3186            .collect::<Vec<_>>()
3187        );
3188    }
3189
3190    #[test]
3191    fn test_profiles() {
3192        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
3193        let parsed: Relations = input.parse().unwrap();
3194        assert_eq!(parsed.to_string(), input);
3195        assert_eq!(parsed.entries().count(), 2);
3196        let entry = parsed.entries().next().unwrap();
3197        assert_eq!(
3198            entry.to_string(),
3199            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3200        );
3201        assert_eq!(entry.relations().count(), 1);
3202        let relation = entry.relations().next().unwrap();
3203        assert_eq!(
3204            relation.to_string(),
3205            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3206        );
3207        assert_eq!(
3208            relation.version(),
3209            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
3210        );
3211        assert_eq!(
3212            relation.architectures().unwrap().collect::<Vec<_>>(),
3213            vec!["i386", "arm"]
3214                .into_iter()
3215                .map(|s| s.to_string())
3216                .collect::<Vec<_>>()
3217        );
3218        assert_eq!(
3219            relation.profiles().collect::<Vec<_>>(),
3220            vec![
3221                vec![BuildProfile::Disabled("nocheck".to_string())],
3222                vec![BuildProfile::Disabled("cross".to_string())]
3223            ]
3224        );
3225    }
3226
3227    #[test]
3228    fn test_substvar() {
3229        let input = "${shlibs:Depends}";
3230
3231        let (parsed, errors) = Relations::parse_relaxed(input, true);
3232        assert_eq!(errors, Vec::<String>::new());
3233        assert_eq!(parsed.to_string(), input);
3234        assert_eq!(parsed.entries().count(), 0);
3235
3236        assert_eq!(
3237            parsed.substvars().collect::<Vec<_>>(),
3238            vec!["${shlibs:Depends}"]
3239        );
3240    }
3241
3242    #[test]
3243    fn test_new() {
3244        let r = Relation::new(
3245            "samba",
3246            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3247        );
3248
3249        assert_eq!(r.to_string(), "samba (>= 2.0)");
3250    }
3251
3252    #[test]
3253    fn test_drop_constraint() {
3254        let mut r = Relation::new(
3255            "samba",
3256            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3257        );
3258
3259        r.drop_constraint();
3260
3261        assert_eq!(r.to_string(), "samba");
3262    }
3263
3264    #[test]
3265    fn test_simple() {
3266        let r = Relation::simple("samba");
3267
3268        assert_eq!(r.to_string(), "samba");
3269    }
3270
3271    #[test]
3272    fn test_remove_first_entry() {
3273        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3274            .parse()
3275            .unwrap();
3276        let removed = rels.remove_entry(0);
3277        assert_eq!(removed.to_string(), "python3-dulwich (>= 0.20.21)");
3278        assert_eq!(rels.to_string(), "python3-dulwich (<< 0.21)");
3279    }
3280
3281    #[test]
3282    fn test_remove_last_entry() {
3283        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3284            .parse()
3285            .unwrap();
3286        rels.remove_entry(1);
3287        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3288    }
3289
3290    #[test]
3291    fn test_remove_middle() {
3292        let mut rels: Relations =
3293            r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21), python3-dulwich (<< 0.22)"#
3294                .parse()
3295                .unwrap();
3296        rels.remove_entry(1);
3297        assert_eq!(
3298            rels.to_string(),
3299            "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.22)"
3300        );
3301    }
3302
3303    #[test]
3304    fn test_remove_added() {
3305        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3306        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3307        rels.push(entry);
3308        rels.remove_entry(1);
3309        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3310    }
3311
3312    #[test]
3313    fn test_push() {
3314        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3315        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3316        rels.push(entry);
3317        assert_eq!(
3318            rels.to_string(),
3319            "python3-dulwich (>= 0.20.21), python3-dulwich"
3320        );
3321    }
3322
3323    #[test]
3324    fn test_insert_with_custom_separator() {
3325        let mut rels: Relations = "python3".parse().unwrap();
3326        let entry = Entry::from(vec![Relation::simple("debhelper")]);
3327        rels.insert_with_separator(1, entry, Some("\n "));
3328        assert_eq!(rels.to_string(), "python3,\n debhelper");
3329    }
3330
3331    #[test]
3332    fn test_insert_with_wrap_and_sort_separator() {
3333        let mut rels: Relations = "python3".parse().unwrap();
3334        let entry = Entry::from(vec![Relation::simple("rustc")]);
3335        // Simulate wrap-and-sort -a style with field name "Depends: " (9 chars)
3336        rels.insert_with_separator(1, entry, Some("\n         "));
3337        assert_eq!(rels.to_string(), "python3,\n         rustc");
3338    }
3339
3340    #[test]
3341    fn test_push_from_empty() {
3342        let mut rels: Relations = "".parse().unwrap();
3343        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3344        rels.push(entry);
3345        assert_eq!(rels.to_string(), "python3-dulwich");
3346    }
3347
3348    #[test]
3349    fn test_insert() {
3350        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3351            .parse()
3352            .unwrap();
3353        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3354        rels.insert(1, entry);
3355        assert_eq!(
3356            rels.to_string(),
3357            "python3-dulwich (>= 0.20.21), python3-dulwich, python3-dulwich (<< 0.21)"
3358        );
3359    }
3360
3361    #[test]
3362    fn test_insert_at_start() {
3363        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3364            .parse()
3365            .unwrap();
3366        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3367        rels.insert(0, entry);
3368        assert_eq!(
3369            rels.to_string(),
3370            "python3-dulwich, python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3371        );
3372    }
3373
3374    #[test]
3375    fn test_insert_after_error() {
3376        let (mut rels, errors) = Relations::parse_relaxed("@foo@, debhelper (>= 1.0)", false);
3377        assert_eq!(
3378            errors,
3379            vec![
3380                "expected $ or identifier but got ERROR",
3381                "expected comma or end of file but got Some(IDENT)",
3382                "expected $ or identifier but got ERROR"
3383            ]
3384        );
3385        let entry = Entry::from(vec![Relation::simple("bar")]);
3386        rels.push(entry);
3387        assert_eq!(rels.to_string(), "@foo@, debhelper (>= 1.0), bar");
3388    }
3389
3390    #[test]
3391    fn test_insert_before_error() {
3392        let (mut rels, errors) = Relations::parse_relaxed("debhelper (>= 1.0), @foo@, bla", false);
3393        assert_eq!(
3394            errors,
3395            vec![
3396                "expected $ or identifier but got ERROR",
3397                "expected comma or end of file but got Some(IDENT)",
3398                "expected $ or identifier but got ERROR"
3399            ]
3400        );
3401        let entry = Entry::from(vec![Relation::simple("bar")]);
3402        rels.insert(0, entry);
3403        assert_eq!(rels.to_string(), "bar, debhelper (>= 1.0), @foo@, bla");
3404    }
3405
3406    #[test]
3407    fn test_replace() {
3408        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3409            .parse()
3410            .unwrap();
3411        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3412        rels.replace(1, entry);
3413        assert_eq!(
3414            rels.to_string(),
3415            "python3-dulwich (>= 0.20.21), python3-dulwich"
3416        );
3417    }
3418
3419    #[test]
3420    fn test_relation_from_entries() {
3421        let entries = vec![
3422            Entry::from(vec![Relation::simple("python3-dulwich")]),
3423            Entry::from(vec![Relation::simple("python3-breezy")]),
3424        ];
3425        let rels: Relations = entries.into();
3426        assert_eq!(rels.entries().count(), 2);
3427        assert_eq!(rels.to_string(), "python3-dulwich, python3-breezy");
3428    }
3429
3430    #[test]
3431    fn test_entry_from_relations() {
3432        let relations = vec![
3433            Relation::simple("python3-dulwich"),
3434            Relation::simple("python3-breezy"),
3435        ];
3436        let entry: Entry = relations.into();
3437        assert_eq!(entry.relations().count(), 2);
3438        assert_eq!(entry.to_string(), "python3-dulwich | python3-breezy");
3439    }
3440
3441    #[test]
3442    fn test_parse_entry() {
3443        let parsed: Entry = "python3-dulwich (>= 0.20.21) | bar".parse().unwrap();
3444        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21) | bar");
3445        assert_eq!(parsed.relations().count(), 2);
3446
3447        assert_eq!(
3448            "foo, bar".parse::<Entry>().unwrap_err(),
3449            "Multiple entries found"
3450        );
3451        assert_eq!("".parse::<Entry>().unwrap_err(), "No entry found");
3452    }
3453
3454    #[test]
3455    fn test_parse_relation() {
3456        let parsed: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3457        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
3458        assert_eq!(
3459            parsed.version(),
3460            Some((
3461                VersionConstraint::GreaterThanEqual,
3462                "0.20.21".parse().unwrap()
3463            ))
3464        );
3465        assert_eq!(
3466            "foo | bar".parse::<Relation>().unwrap_err(),
3467            "Multiple relations found"
3468        );
3469        assert_eq!("".parse::<Relation>().unwrap_err(), "No entry found");
3470    }
3471
3472    #[test]
3473    fn test_special() {
3474        let parsed: Relation = "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3475            .parse()
3476            .unwrap();
3477        assert_eq!(
3478            parsed.to_string(),
3479            "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3480        );
3481        assert_eq!(
3482            parsed.version(),
3483            Some((
3484                VersionConstraint::GreaterThanEqual,
3485                "0.1.138-~~".parse().unwrap()
3486            ))
3487        );
3488        assert_eq!(parsed.archqual(), Some("amd64".to_string()));
3489        assert_eq!(parsed.name(), "librust-breezyshim+dirty-tracker-dev");
3490    }
3491
3492    #[test]
3493    fn test_relations_satisfied_by() {
3494        let rels: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3495            .parse()
3496            .unwrap();
3497        let satisfied = |name: &str| -> Option<debversion::Version> {
3498            match name {
3499                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3500                _ => None,
3501            }
3502        };
3503        assert!(rels.satisfied_by(satisfied));
3504
3505        let satisfied = |name: &str| match name {
3506            "python3-dulwich" => Some("0.21".parse().unwrap()),
3507            _ => None,
3508        };
3509        assert!(!rels.satisfied_by(satisfied));
3510
3511        let satisfied = |name: &str| match name {
3512            "python3-dulwich" => Some("0.20.20".parse().unwrap()),
3513            _ => None,
3514        };
3515        assert!(!rels.satisfied_by(satisfied));
3516    }
3517
3518    #[test]
3519    fn test_entry_satisfied_by() {
3520        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3521            .parse()
3522            .unwrap();
3523        let satisfied = |name: &str| -> Option<debversion::Version> {
3524            match name {
3525                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3526                _ => None,
3527            }
3528        };
3529        assert!(entry.satisfied_by(satisfied));
3530        let satisfied = |name: &str| -> Option<debversion::Version> {
3531            match name {
3532                "python3-dulwich" => Some("0.18".parse().unwrap()),
3533                _ => None,
3534            }
3535        };
3536        assert!(!entry.satisfied_by(satisfied));
3537    }
3538
3539    #[test]
3540    fn test_wrap_and_sort_relation() {
3541        let relation: Relation = "   python3-dulwich   (>= 11) [  amd64 ] <  lala>"
3542            .parse()
3543            .unwrap();
3544
3545        let wrapped = relation.wrap_and_sort();
3546
3547        assert_eq!(
3548            wrapped.to_string(),
3549            "python3-dulwich (>= 11) [amd64] <lala>"
3550        );
3551    }
3552
3553    #[test]
3554    fn test_wrap_and_sort_relations() {
3555        let entry: Relations =
3556            "python3-dulwich (>= 0.20.21)   | bar, \n\n\n\npython3-dulwich (<< 0.21)"
3557                .parse()
3558                .unwrap();
3559
3560        let wrapped = entry.wrap_and_sort();
3561
3562        assert_eq!(
3563            wrapped.to_string(),
3564            "bar | python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3565        );
3566    }
3567
3568    #[cfg(feature = "serde")]
3569    #[test]
3570    fn test_serialize_relations() {
3571        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3572            .parse()
3573            .unwrap();
3574        let serialized = serde_json::to_string(&relations).unwrap();
3575        assert_eq!(
3576            serialized,
3577            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
3578        );
3579    }
3580
3581    #[cfg(feature = "serde")]
3582    #[test]
3583    fn test_deserialize_relations() {
3584        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3585            .parse()
3586            .unwrap();
3587        let serialized = serde_json::to_string(&relations).unwrap();
3588        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
3589        assert_eq!(deserialized.to_string(), relations.to_string());
3590    }
3591
3592    #[cfg(feature = "serde")]
3593    #[test]
3594    fn test_serialize_relation() {
3595        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3596        let serialized = serde_json::to_string(&relation).unwrap();
3597        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
3598    }
3599
3600    #[cfg(feature = "serde")]
3601    #[test]
3602    fn test_deserialize_relation() {
3603        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3604        let serialized = serde_json::to_string(&relation).unwrap();
3605        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
3606        assert_eq!(deserialized.to_string(), relation.to_string());
3607    }
3608
3609    #[cfg(feature = "serde")]
3610    #[test]
3611    fn test_serialize_entry() {
3612        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3613            .parse()
3614            .unwrap();
3615        let serialized = serde_json::to_string(&entry).unwrap();
3616        assert_eq!(
3617            serialized,
3618            r#""python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)""#
3619        );
3620    }
3621
3622    #[cfg(feature = "serde")]
3623    #[test]
3624    fn test_deserialize_entry() {
3625        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3626            .parse()
3627            .unwrap();
3628        let serialized = serde_json::to_string(&entry).unwrap();
3629        let deserialized: Entry = serde_json::from_str(&serialized).unwrap();
3630        assert_eq!(deserialized.to_string(), entry.to_string());
3631    }
3632
3633    #[test]
3634    fn test_remove_first_relation() {
3635        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3636            .parse()
3637            .unwrap();
3638        let mut rel = entry.relations().next().unwrap();
3639        rel.remove();
3640        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.18)");
3641    }
3642
3643    #[test]
3644    fn test_remove_last_relation() {
3645        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3646            .parse()
3647            .unwrap();
3648        let mut rel = entry.relations().nth(1).unwrap();
3649        rel.remove();
3650        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3651    }
3652
3653    #[test]
3654    fn test_remove_only_relation() {
3655        let entry: Entry = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3656        let mut rel = entry.relations().next().unwrap();
3657        rel.remove();
3658        assert_eq!(entry.to_string(), "");
3659    }
3660
3661    #[test]
3662    fn test_relations_is_empty() {
3663        let entry: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3664        assert!(!entry.is_empty());
3665        assert_eq!(1, entry.len());
3666        let mut rel = entry.entries().next().unwrap();
3667        rel.remove();
3668        assert!(entry.is_empty());
3669        assert_eq!(0, entry.len());
3670    }
3671
3672    #[test]
3673    fn test_entry_is_empty() {
3674        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3675            .parse()
3676            .unwrap();
3677        assert!(!entry.is_empty());
3678        assert_eq!(2, entry.len());
3679        let mut rel = entry.relations().next().unwrap();
3680        rel.remove();
3681        assert!(!entry.is_empty());
3682        assert_eq!(1, entry.len());
3683        let mut rel = entry.relations().next().unwrap();
3684        rel.remove();
3685        assert!(entry.is_empty());
3686        assert_eq!(0, entry.len());
3687    }
3688
3689    #[test]
3690    fn test_relation_set_version() {
3691        let mut rel: Relation = "samba".parse().unwrap();
3692        rel.set_version(None);
3693        assert_eq!("samba", rel.to_string());
3694        rel.set_version(Some((
3695            VersionConstraint::GreaterThanEqual,
3696            "2.0".parse().unwrap(),
3697        )));
3698        assert_eq!("samba (>= 2.0)", rel.to_string());
3699        rel.set_version(None);
3700        assert_eq!("samba", rel.to_string());
3701        rel.set_version(Some((
3702            VersionConstraint::GreaterThanEqual,
3703            "2.0".parse().unwrap(),
3704        )));
3705        rel.set_version(Some((
3706            VersionConstraint::GreaterThanEqual,
3707            "1.1".parse().unwrap(),
3708        )));
3709        assert_eq!("samba (>= 1.1)", rel.to_string());
3710    }
3711
3712    #[test]
3713    fn test_replace_relation() {
3714        let mut entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3715            .parse()
3716            .unwrap();
3717        let new_rel = Relation::simple("python3-breezy");
3718        entry.replace(0, new_rel);
3719        assert_eq!(
3720            entry.to_string(),
3721            "python3-breezy | python3-dulwich (<< 0.18)"
3722        );
3723    }
3724
3725    #[test]
3726    fn test_entry_push_relation() {
3727        let relations: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3728        let new_rel = Relation::simple("python3-breezy");
3729        let mut entry = relations.entries().next().unwrap();
3730        entry.push(new_rel);
3731        assert_eq!(
3732            entry.to_string(),
3733            "python3-dulwich (>= 0.20.21) | python3-breezy"
3734        );
3735        assert_eq!(
3736            relations.to_string(),
3737            "python3-dulwich (>= 0.20.21) | python3-breezy"
3738        );
3739    }
3740
3741    #[test]
3742    fn test_relations_remove_empty_entry() {
3743        let (mut relations, errors) = Relations::parse_relaxed("foo, , bar, ", false);
3744        assert_eq!(errors, Vec::<String>::new());
3745        assert_eq!(relations.to_string(), "foo, , bar, ");
3746        assert_eq!(relations.len(), 2);
3747        assert_eq!(
3748            relations.entries().next().unwrap().to_string(),
3749            "foo".to_string()
3750        );
3751        assert_eq!(
3752            relations.entries().nth(1).unwrap().to_string(),
3753            "bar".to_string()
3754        );
3755        relations.remove_entry(1);
3756        assert_eq!(relations.to_string(), "foo, , ");
3757    }
3758
3759    #[test]
3760    fn test_entry_remove_relation() {
3761        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3762        let removed = entry.remove_relation(0);
3763        assert_eq!(removed.to_string(), "python3-dulwich");
3764        assert_eq!(entry.to_string(), "samba");
3765    }
3766
3767    #[test]
3768    fn test_wrap_and_sort_removes_empty_entries() {
3769        let relations: Relations = "foo, , bar, ".parse().unwrap();
3770        let wrapped = relations.wrap_and_sort();
3771        assert_eq!(wrapped.to_string(), "bar, foo");
3772    }
3773
3774    #[test]
3775    fn test_set_archqual() {
3776        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3777        let mut rel = entry.relations().next().unwrap();
3778        rel.set_archqual("amd64");
3779        assert_eq!(rel.to_string(), "python3-dulwich:amd64");
3780        assert_eq!(rel.archqual(), Some("amd64".to_string()));
3781        assert_eq!(entry.to_string(), "python3-dulwich:amd64 | samba");
3782        rel.set_archqual("i386");
3783        assert_eq!(rel.to_string(), "python3-dulwich:i386");
3784        assert_eq!(rel.archqual(), Some("i386".to_string()));
3785        assert_eq!(entry.to_string(), "python3-dulwich:i386 | samba");
3786    }
3787
3788    #[test]
3789    fn test_set_architectures() {
3790        let mut relation = Relation::simple("samba");
3791        relation.set_architectures(vec!["amd64", "i386"].into_iter());
3792        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3793    }
3794
3795    #[test]
3796    fn test_relation_builder_no_architectures() {
3797        // Test that building a relation without architectures doesn't add empty brackets
3798        let relation = Relation::build("debhelper").build();
3799        assert_eq!(relation.to_string(), "debhelper");
3800    }
3801
3802    #[test]
3803    fn test_relation_builder_with_architectures() {
3804        // Test that building a relation with architectures works correctly
3805        let relation = Relation::build("samba")
3806            .architectures(vec!["amd64".to_string(), "i386".to_string()])
3807            .build();
3808        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3809    }
3810
3811    #[test]
3812    fn test_ensure_minimum_version_add_new() {
3813        let mut relations: Relations = "python3".parse().unwrap();
3814        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3815        assert_eq!(relations.to_string(), "python3, debhelper (>= 12)");
3816    }
3817
3818    #[test]
3819    fn test_ensure_minimum_version_update() {
3820        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3821        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3822        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3823    }
3824
3825    #[test]
3826    fn test_ensure_minimum_version_no_change() {
3827        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
3828        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3829        assert_eq!(relations.to_string(), "debhelper (>= 13)");
3830    }
3831
3832    #[test]
3833    fn test_ensure_minimum_version_no_version() {
3834        let mut relations: Relations = "debhelper".parse().unwrap();
3835        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3836        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3837    }
3838
3839    #[test]
3840    fn test_ensure_minimum_version_preserves_newline() {
3841        // Test that newline after the field name is preserved
3842        // This is the format often used in Debian control files:
3843        // Build-Depends:
3844        //  debhelper (>= 9),
3845        //  pkg-config
3846        let input = "\n debhelper (>= 9),\n pkg-config,\n uuid-dev";
3847        let mut relations: Relations = input.parse().unwrap();
3848        relations.ensure_minimum_version("debhelper", &"12~".parse().unwrap());
3849        let result = relations.to_string();
3850
3851        // The newline before the first entry should be preserved
3852        assert!(
3853            result.starts_with('\n'),
3854            "Expected result to start with newline, got: {:?}",
3855            result
3856        );
3857        assert_eq!(result, "\n debhelper (>= 12~),\n pkg-config,\n uuid-dev");
3858    }
3859
3860    #[test]
3861    fn test_ensure_minimum_version_preserves_newline_in_control() {
3862        // Test the full scenario from the bug report
3863        use crate::lossless::Control;
3864        use std::str::FromStr;
3865
3866        let input = r#"Source: f2fs-tools
3867Section: admin
3868Priority: optional
3869Maintainer: Test <test@example.com>
3870Build-Depends:
3871 debhelper (>= 9),
3872 pkg-config,
3873 uuid-dev
3874
3875Package: f2fs-tools
3876Description: test
3877"#;
3878
3879        let control = Control::from_str(input).unwrap();
3880        let mut source = control.source().unwrap();
3881        let mut build_depends = source.build_depends().unwrap();
3882
3883        let version = Version::from_str("12~").unwrap();
3884        build_depends.ensure_minimum_version("debhelper", &version);
3885
3886        source.set_build_depends(&build_depends);
3887
3888        let output = control.to_string();
3889
3890        // Check that the formatting is preserved - the newline after "Build-Depends:" should still be there
3891        assert!(
3892            output.contains("Build-Depends:\n debhelper (>= 12~)"),
3893            "Expected 'Build-Depends:\\n debhelper (>= 12~)' but got:\n{}",
3894            output
3895        );
3896    }
3897
3898    #[test]
3899    fn test_ensure_exact_version_add_new() {
3900        let mut relations: Relations = "python3".parse().unwrap();
3901        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3902        assert_eq!(relations.to_string(), "python3, debhelper (= 12)");
3903    }
3904
3905    #[test]
3906    fn test_ensure_exact_version_update() {
3907        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3908        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3909        assert_eq!(relations.to_string(), "debhelper (= 12)");
3910    }
3911
3912    #[test]
3913    fn test_ensure_exact_version_no_change() {
3914        let mut relations: Relations = "debhelper (= 12)".parse().unwrap();
3915        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3916        assert_eq!(relations.to_string(), "debhelper (= 12)");
3917    }
3918
3919    #[test]
3920    fn test_ensure_some_version_add_new() {
3921        let mut relations: Relations = "python3".parse().unwrap();
3922        relations.ensure_some_version("debhelper");
3923        assert_eq!(relations.to_string(), "python3, debhelper");
3924    }
3925
3926    #[test]
3927    fn test_ensure_some_version_exists_with_version() {
3928        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
3929        relations.ensure_some_version("debhelper");
3930        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3931    }
3932
3933    #[test]
3934    fn test_ensure_some_version_exists_no_version() {
3935        let mut relations: Relations = "debhelper".parse().unwrap();
3936        relations.ensure_some_version("debhelper");
3937        assert_eq!(relations.to_string(), "debhelper");
3938    }
3939
3940    #[test]
3941    fn test_ensure_substvar() {
3942        let mut relations: Relations = "python3".parse().unwrap();
3943        relations.ensure_substvar("${misc:Depends}").unwrap();
3944        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
3945    }
3946
3947    #[test]
3948    fn test_ensure_substvar_already_exists() {
3949        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
3950        relations.ensure_substvar("${misc:Depends}").unwrap();
3951        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
3952    }
3953
3954    #[test]
3955    fn test_ensure_substvar_empty_relations() {
3956        let mut relations: Relations = Relations::new();
3957        relations.ensure_substvar("${misc:Depends}").unwrap();
3958        assert_eq!(relations.to_string(), "${misc:Depends}");
3959    }
3960
3961    #[test]
3962    fn test_ensure_substvar_preserves_whitespace() {
3963        // Test with non-standard whitespace (multiple spaces)
3964        let (mut relations, _) = Relations::parse_relaxed("python3,  rustc", false);
3965        relations.ensure_substvar("${misc:Depends}").unwrap();
3966        // Should preserve the double-space pattern
3967        assert_eq!(relations.to_string(), "python3,  rustc,  ${misc:Depends}");
3968    }
3969
3970    #[test]
3971    fn test_ensure_substvar_to_existing_substvar() {
3972        // Test adding a substvar to existing substvar (no entries)
3973        // This reproduces the bug where space after comma is lost
3974        let (mut relations, _) = Relations::parse_relaxed("${shlibs:Depends}", true);
3975        relations.ensure_substvar("${misc:Depends}").unwrap();
3976        // Should have a space after the comma
3977        assert_eq!(relations.to_string(), "${shlibs:Depends}, ${misc:Depends}");
3978    }
3979
3980    #[test]
3981    fn test_drop_substvar_basic() {
3982        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
3983        relations.drop_substvar("${misc:Depends}");
3984        assert_eq!(relations.to_string(), "python3");
3985    }
3986
3987    #[test]
3988    fn test_drop_substvar_first_position() {
3989        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}, python3", true);
3990        relations.drop_substvar("${misc:Depends}");
3991        assert_eq!(relations.to_string(), "python3");
3992    }
3993
3994    #[test]
3995    fn test_drop_substvar_middle_position() {
3996        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
3997        relations.drop_substvar("${misc:Depends}");
3998        assert_eq!(relations.to_string(), "python3, rustc");
3999    }
4000
4001    #[test]
4002    fn test_drop_substvar_only_substvar() {
4003        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}", true);
4004        relations.drop_substvar("${misc:Depends}");
4005        assert_eq!(relations.to_string(), "");
4006    }
4007
4008    #[test]
4009    fn test_drop_substvar_not_exists() {
4010        let (mut relations, _) = Relations::parse_relaxed("python3, rustc", false);
4011        relations.drop_substvar("${misc:Depends}");
4012        assert_eq!(relations.to_string(), "python3, rustc");
4013    }
4014
4015    #[test]
4016    fn test_drop_substvar_multiple_substvars() {
4017        let (mut relations, _) =
4018            Relations::parse_relaxed("python3, ${misc:Depends}, ${shlibs:Depends}", true);
4019        relations.drop_substvar("${misc:Depends}");
4020        assert_eq!(relations.to_string(), "python3, ${shlibs:Depends}");
4021    }
4022
4023    #[test]
4024    fn test_drop_substvar_preserves_whitespace() {
4025        let (mut relations, _) = Relations::parse_relaxed("python3,  ${misc:Depends}", true);
4026        relations.drop_substvar("${misc:Depends}");
4027        assert_eq!(relations.to_string(), "python3");
4028    }
4029
4030    #[test]
4031    fn test_filter_entries_basic() {
4032        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4033        relations.filter_entries(|entry| entry.relations().any(|r| r.name().starts_with("python")));
4034        assert_eq!(relations.to_string(), "python3");
4035    }
4036
4037    #[test]
4038    fn test_filter_entries_keep_all() {
4039        let mut relations: Relations = "python3, debhelper".parse().unwrap();
4040        relations.filter_entries(|_| true);
4041        assert_eq!(relations.to_string(), "python3, debhelper");
4042    }
4043
4044    #[test]
4045    fn test_filter_entries_remove_all() {
4046        let mut relations: Relations = "python3, debhelper".parse().unwrap();
4047        relations.filter_entries(|_| false);
4048        assert_eq!(relations.to_string(), "");
4049    }
4050
4051    #[test]
4052    fn test_filter_entries_keep_middle() {
4053        let mut relations: Relations = "aaa, bbb, ccc".parse().unwrap();
4054        relations.filter_entries(|entry| entry.relations().any(|r| r.name() == "bbb"));
4055        assert_eq!(relations.to_string(), "bbb");
4056    }
4057
4058    // Tests for new convenience methods
4059
4060    #[test]
4061    fn test_is_sorted_wrap_and_sort_order() {
4062        // Sorted according to WrapAndSortOrder
4063        let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
4064        assert!(relations.is_sorted(&WrapAndSortOrder));
4065
4066        // Not sorted
4067        let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
4068        assert!(!relations.is_sorted(&WrapAndSortOrder));
4069
4070        // Build systems first (sorted alphabetically within their group)
4071        let (relations, _) =
4072            Relations::parse_relaxed("cdbs, debhelper-compat, python3, ${misc:Depends}", true);
4073        assert!(relations.is_sorted(&WrapAndSortOrder));
4074    }
4075
4076    #[test]
4077    fn test_is_sorted_default_order() {
4078        // Sorted alphabetically
4079        let relations: Relations = "aaa, bbb, ccc".parse().unwrap();
4080        assert!(relations.is_sorted(&DefaultSortingOrder));
4081
4082        // Not sorted
4083        let relations: Relations = "ccc, aaa, bbb".parse().unwrap();
4084        assert!(!relations.is_sorted(&DefaultSortingOrder));
4085
4086        // Special items at end
4087        let (relations, _) = Relations::parse_relaxed("aaa, bbb, ${misc:Depends}", true);
4088        assert!(relations.is_sorted(&DefaultSortingOrder));
4089    }
4090
4091    #[test]
4092    fn test_is_sorted_with_substvars() {
4093        // Substvars should be ignored by DefaultSortingOrder
4094        let (relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
4095        // This is considered sorted because ${misc:Depends} is ignored
4096        assert!(relations.is_sorted(&DefaultSortingOrder));
4097    }
4098
4099    #[test]
4100    fn test_drop_dependency_exists() {
4101        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4102        assert!(relations.drop_dependency("debhelper"));
4103        assert_eq!(relations.to_string(), "python3, rustc");
4104    }
4105
4106    #[test]
4107    fn test_drop_dependency_not_exists() {
4108        let mut relations: Relations = "python3, rustc".parse().unwrap();
4109        assert!(!relations.drop_dependency("nonexistent"));
4110        assert_eq!(relations.to_string(), "python3, rustc");
4111    }
4112
4113    #[test]
4114    fn test_drop_dependency_only_item() {
4115        let mut relations: Relations = "python3".parse().unwrap();
4116        assert!(relations.drop_dependency("python3"));
4117        assert_eq!(relations.to_string(), "");
4118    }
4119
4120    #[test]
4121    fn test_add_dependency_to_empty() {
4122        let mut relations: Relations = "".parse().unwrap();
4123        let entry = Entry::from(Relation::simple("python3"));
4124        relations.add_dependency(entry, None);
4125        assert_eq!(relations.to_string(), "python3");
4126    }
4127
4128    #[test]
4129    fn test_add_dependency_sorted_position() {
4130        let mut relations: Relations = "debhelper, rustc".parse().unwrap();
4131        let entry = Entry::from(Relation::simple("python3"));
4132        relations.add_dependency(entry, None);
4133        // Should be inserted in sorted position
4134        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4135    }
4136
4137    #[test]
4138    fn test_add_dependency_explicit_position() {
4139        let mut relations: Relations = "python3, rustc".parse().unwrap();
4140        let entry = Entry::from(Relation::simple("debhelper"));
4141        relations.add_dependency(entry, Some(0));
4142        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4143    }
4144
4145    #[test]
4146    fn test_add_dependency_build_system_first() {
4147        let mut relations: Relations = "python3, rustc".parse().unwrap();
4148        let entry = Entry::from(Relation::simple("debhelper-compat"));
4149        relations.add_dependency(entry, None);
4150        // debhelper-compat should be inserted first (build system)
4151        assert_eq!(relations.to_string(), "debhelper-compat, python3, rustc");
4152    }
4153
4154    #[test]
4155    fn test_add_dependency_at_end() {
4156        let mut relations: Relations = "debhelper, python3".parse().unwrap();
4157        let entry = Entry::from(Relation::simple("zzz-package"));
4158        relations.add_dependency(entry, None);
4159        // Should be added at the end (alphabetically after python3)
4160        assert_eq!(relations.to_string(), "debhelper, python3, zzz-package");
4161    }
4162
4163    #[test]
4164    fn test_add_dependency_to_single_entry() {
4165        // Regression test: ensure comma is added when inserting into single-entry Relations
4166        let mut relations: Relations = "python3-dulwich".parse().unwrap();
4167        let entry: Entry = "debhelper-compat (= 12)".parse().unwrap();
4168        relations.add_dependency(entry, None);
4169        // Should insert with comma separator
4170        assert_eq!(
4171            relations.to_string(),
4172            "debhelper-compat (= 12), python3-dulwich"
4173        );
4174    }
4175
4176    #[test]
4177    fn test_get_relation_exists() {
4178        let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
4179        let result = relations.get_relation("debhelper");
4180        assert!(result.is_ok());
4181        let (idx, entry) = result.unwrap();
4182        assert_eq!(idx, 1);
4183        assert_eq!(entry.to_string(), "debhelper (>= 12)");
4184    }
4185
4186    #[test]
4187    fn test_get_relation_not_exists() {
4188        let relations: Relations = "python3, rustc".parse().unwrap();
4189        let result = relations.get_relation("nonexistent");
4190        assert_eq!(result, Err("Package nonexistent not found".to_string()));
4191    }
4192
4193    #[test]
4194    fn test_get_relation_complex_rule() {
4195        let relations: Relations = "python3 | python3-minimal, rustc".parse().unwrap();
4196        let result = relations.get_relation("python3");
4197        assert_eq!(
4198            result,
4199            Err("Complex rule for python3, aborting".to_string())
4200        );
4201    }
4202
4203    #[test]
4204    fn test_iter_relations_for_simple() {
4205        let relations: Relations = "python3, debhelper, python3-dev".parse().unwrap();
4206        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4207        assert_eq!(entries.len(), 1);
4208        assert_eq!(entries[0].0, 0);
4209        assert_eq!(entries[0].1.to_string(), "python3");
4210    }
4211
4212    #[test]
4213    fn test_iter_relations_for_alternatives() {
4214        let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
4215        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4216        // Should find both the alternative entry and python3-dev is not included
4217        assert_eq!(entries.len(), 1);
4218        assert_eq!(entries[0].0, 0);
4219    }
4220
4221    #[test]
4222    fn test_iter_relations_for_not_found() {
4223        let relations: Relations = "python3, rustc".parse().unwrap();
4224        let entries: Vec<_> = relations.iter_relations_for("debhelper").collect();
4225        assert_eq!(entries.len(), 0);
4226    }
4227
4228    #[test]
4229    fn test_has_relation_exists() {
4230        let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4231        assert!(relations.has_relation("debhelper"));
4232        assert!(relations.has_relation("python3"));
4233        assert!(relations.has_relation("rustc"));
4234    }
4235
4236    #[test]
4237    fn test_has_relation_not_exists() {
4238        let relations: Relations = "python3, rustc".parse().unwrap();
4239        assert!(!relations.has_relation("debhelper"));
4240    }
4241
4242    #[test]
4243    fn test_has_relation_in_alternative() {
4244        let relations: Relations = "python3 | python3-minimal".parse().unwrap();
4245        assert!(relations.has_relation("python3"));
4246        assert!(relations.has_relation("python3-minimal"));
4247    }
4248
4249    #[test]
4250    fn test_sorting_order_wrap_and_sort_build_systems() {
4251        let order = WrapAndSortOrder;
4252        // Build systems should come before regular packages
4253        assert!(order.lt("debhelper", "python3"));
4254        assert!(order.lt("debhelper-compat", "rustc"));
4255        assert!(order.lt("cdbs", "aaa"));
4256        assert!(order.lt("dh-python", "python3"));
4257    }
4258
4259    #[test]
4260    fn test_sorting_order_wrap_and_sort_regular_packages() {
4261        let order = WrapAndSortOrder;
4262        // Regular packages sorted alphabetically
4263        assert!(order.lt("aaa", "bbb"));
4264        assert!(order.lt("python3", "rustc"));
4265        assert!(!order.lt("rustc", "python3"));
4266    }
4267
4268    #[test]
4269    fn test_sorting_order_wrap_and_sort_substvars() {
4270        let order = WrapAndSortOrder;
4271        // Substvars should come after regular packages
4272        assert!(order.lt("python3", "${misc:Depends}"));
4273        assert!(!order.lt("${misc:Depends}", "python3"));
4274        // But wrap-and-sort doesn't ignore them
4275        assert!(!order.ignore("${misc:Depends}"));
4276    }
4277
4278    #[test]
4279    fn test_sorting_order_default_special_items() {
4280        let order = DefaultSortingOrder;
4281        // Special items should come after regular items
4282        assert!(order.lt("python3", "${misc:Depends}"));
4283        assert!(order.lt("aaa", "@cdbs@"));
4284        // And should be ignored
4285        assert!(order.ignore("${misc:Depends}"));
4286        assert!(order.ignore("@cdbs@"));
4287        assert!(!order.ignore("python3"));
4288    }
4289
4290    #[test]
4291    fn test_is_special_package_name() {
4292        assert!(is_special_package_name("${misc:Depends}"));
4293        assert!(is_special_package_name("${shlibs:Depends}"));
4294        assert!(is_special_package_name("@cdbs@"));
4295        assert!(!is_special_package_name("python3"));
4296        assert!(!is_special_package_name("debhelper"));
4297    }
4298
4299    #[test]
4300    fn test_add_dependency_with_explicit_position() {
4301        // Test that add_dependency works with explicit position and preserves whitespace
4302        let mut relations: Relations = "python3,  rustc".parse().unwrap();
4303        let entry = Entry::from(Relation::simple("debhelper"));
4304        relations.add_dependency(entry, Some(1));
4305        // Should preserve the 2-space pattern from the original
4306        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc");
4307    }
4308
4309    #[test]
4310    fn test_whitespace_detection_single_space() {
4311        let mut relations: Relations = "python3, rustc".parse().unwrap();
4312        let entry = Entry::from(Relation::simple("debhelper"));
4313        relations.add_dependency(entry, Some(1));
4314        assert_eq!(relations.to_string(), "python3, debhelper, rustc");
4315    }
4316
4317    #[test]
4318    fn test_whitespace_detection_multiple_spaces() {
4319        let mut relations: Relations = "python3,  rustc,  gcc".parse().unwrap();
4320        let entry = Entry::from(Relation::simple("debhelper"));
4321        relations.add_dependency(entry, Some(1));
4322        // Should detect and use the 2-space pattern
4323        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc,  gcc");
4324    }
4325
4326    #[test]
4327    fn test_whitespace_detection_mixed_patterns() {
4328        // When patterns differ, use the most common one
4329        let mut relations: Relations = "a, b, c,  d, e".parse().unwrap();
4330        let entry = Entry::from(Relation::simple("x"));
4331        relations.push(entry);
4332        // Three single-space (after a, b, d), one double-space (after c)
4333        // Should use single space as it's most common
4334        assert_eq!(relations.to_string(), "a, b, c,  d, e, x");
4335    }
4336
4337    #[test]
4338    fn test_whitespace_detection_newlines() {
4339        let mut relations: Relations = "python3,\n rustc".parse().unwrap();
4340        let entry = Entry::from(Relation::simple("debhelper"));
4341        relations.add_dependency(entry, Some(1));
4342        // Detects full pattern including newline
4343        assert_eq!(relations.to_string(), "python3,\n debhelper,\n rustc");
4344    }
4345
4346    #[test]
4347    fn test_append_with_newline_no_trailing() {
4348        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4349        let entry = Entry::from(Relation::simple("blah"));
4350        relations.add_dependency(entry, None);
4351        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4352    }
4353
4354    #[test]
4355    fn test_append_with_trailing_newline() {
4356        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4357        let entry = Entry::from(Relation::simple("blah"));
4358        relations.add_dependency(entry, None);
4359        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4360    }
4361
4362    #[test]
4363    fn test_append_with_4_space_indent() {
4364        let mut relations: Relations = "foo,\n    bar".parse().unwrap();
4365        let entry = Entry::from(Relation::simple("blah"));
4366        relations.add_dependency(entry, None);
4367        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4368    }
4369
4370    #[test]
4371    fn test_append_with_4_space_and_trailing_newline() {
4372        let mut relations: Relations = "foo,\n    bar\n".parse().unwrap();
4373        let entry = Entry::from(Relation::simple("blah"));
4374        relations.add_dependency(entry, None);
4375        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4376    }
4377
4378    #[test]
4379    fn test_odd_syntax_append_no_trailing() {
4380        let mut relations: Relations = "\n foo\n , bar".parse().unwrap();
4381        let entry = Entry::from(Relation::simple("blah"));
4382        relations.add_dependency(entry, None);
4383        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4384    }
4385
4386    #[test]
4387    fn test_odd_syntax_append_with_trailing() {
4388        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4389        let entry = Entry::from(Relation::simple("blah"));
4390        relations.add_dependency(entry, None);
4391        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4392    }
4393
4394    #[test]
4395    fn test_insert_at_1_no_trailing() {
4396        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4397        let entry = Entry::from(Relation::simple("blah"));
4398        relations.add_dependency(entry, Some(1));
4399        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4400    }
4401
4402    #[test]
4403    fn test_insert_at_1_with_trailing() {
4404        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4405        let entry = Entry::from(Relation::simple("blah"));
4406        relations.add_dependency(entry, Some(1));
4407        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4408    }
4409
4410    #[test]
4411    fn test_odd_syntax_insert_at_1() {
4412        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4413        let entry = Entry::from(Relation::simple("blah"));
4414        relations.add_dependency(entry, Some(1));
4415        assert_eq!(relations.to_string(), "\n foo\n , blah\n , bar");
4416    }
4417
4418    #[test]
4419    fn test_relations_preserves_exact_whitespace() {
4420        // Test that Relations preserves exact whitespace from input
4421        let input =
4422            "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], pkg-config";
4423
4424        let relations: Relations = input.parse().unwrap();
4425
4426        // The whitespace should be preserved in the syntax tree
4427        assert_eq!(
4428            relations.to_string(),
4429            input,
4430            "Relations should preserve exact whitespace from input"
4431        );
4432    }
4433
4434    #[test]
4435    fn test_remove_entry_preserves_indentation() {
4436        // Test that removing an entry preserves the indentation pattern
4437        let input = "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config";
4438
4439        let mut relations: Relations = input.parse().unwrap();
4440
4441        // Find and remove dh-systemd entry (index 2)
4442        let mut to_remove = Vec::new();
4443        for (idx, entry) in relations.entries().enumerate() {
4444            for relation in entry.relations() {
4445                if relation.name() == "dh-systemd" {
4446                    to_remove.push(idx);
4447                    break;
4448                }
4449            }
4450        }
4451
4452        for idx in to_remove.into_iter().rev() {
4453            relations.remove_entry(idx);
4454        }
4455
4456        let output = relations.to_string();
4457        println!("After removal: '{}'", output);
4458
4459        // The 4-space indentation should be preserved
4460        assert!(
4461            output.contains("\n    libsystemd-dev"),
4462            "Expected 4-space indentation to be preserved, but got:\n'{}'",
4463            output
4464        );
4465    }
4466
4467    #[test]
4468    fn test_relation_is_implied_by_same_package() {
4469        // Same package name with compatible version constraints
4470        let inner = Relation::new(
4471            "pkg",
4472            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4473        );
4474        let outer = Relation::new(
4475            "pkg",
4476            Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())),
4477        );
4478        assert!(inner.is_implied_by(&outer));
4479    }
4480
4481    #[test]
4482    fn test_relation_is_implied_by_different_package() {
4483        // Different package names should not imply
4484        let inner = Relation::new("pkg1", None);
4485        let outer = Relation::new("pkg2", None);
4486        assert!(!inner.is_implied_by(&outer));
4487    }
4488
4489    #[test]
4490    fn test_relation_is_implied_by_no_version() {
4491        // No version constraint is implied by any version
4492        let inner = Relation::new("pkg", None);
4493        let outer = Relation::new(
4494            "pkg",
4495            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4496        );
4497        assert!(inner.is_implied_by(&outer));
4498    }
4499
4500    #[test]
4501    fn test_relation_is_implied_by_identical() {
4502        // Identical relations imply each other
4503        let inner = Relation::new(
4504            "pkg",
4505            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4506        );
4507        let outer = Relation::new(
4508            "pkg",
4509            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4510        );
4511        assert!(inner.is_implied_by(&outer));
4512        assert!(outer.is_implied_by(&inner));
4513    }
4514
4515    #[test]
4516    fn test_relation_is_implied_by_greater_than_equal() {
4517        // pkg >= 1.0 is implied by pkg >= 2.0
4518        let inner = Relation::new(
4519            "pkg",
4520            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4521        );
4522        let outer = Relation::new(
4523            "pkg",
4524            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
4525        );
4526        assert!(inner.is_implied_by(&outer));
4527        assert!(!outer.is_implied_by(&inner));
4528
4529        // pkg >= 1.0 is implied by pkg = 2.0
4530        let outer = Relation::new(
4531            "pkg",
4532            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4533        );
4534        assert!(inner.is_implied_by(&outer));
4535
4536        // pkg >= 1.0 is implied by pkg >> 1.5
4537        let outer = Relation::new(
4538            "pkg",
4539            Some((VersionConstraint::GreaterThan, "1.5".parse().unwrap())),
4540        );
4541        assert!(inner.is_implied_by(&outer));
4542
4543        // pkg >= 3.0 is NOT implied by pkg >> 3.0 (>> 3.0 doesn't include 3.0 itself)
4544        let inner = Relation::new(
4545            "pkg",
4546            Some((VersionConstraint::GreaterThanEqual, "3.0".parse().unwrap())),
4547        );
4548        let outer = Relation::new(
4549            "pkg",
4550            Some((VersionConstraint::GreaterThan, "3.0".parse().unwrap())),
4551        );
4552        assert!(!inner.is_implied_by(&outer));
4553    }
4554
4555    #[test]
4556    fn test_relation_is_implied_by_less_than_equal() {
4557        // pkg <= 2.0 is implied by pkg <= 1.0
4558        let inner = Relation::new(
4559            "pkg",
4560            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4561        );
4562        let outer = Relation::new(
4563            "pkg",
4564            Some((VersionConstraint::LessThanEqual, "1.0".parse().unwrap())),
4565        );
4566        assert!(inner.is_implied_by(&outer));
4567        assert!(!outer.is_implied_by(&inner));
4568
4569        // pkg <= 2.0 is implied by pkg = 1.0
4570        let outer = Relation::new(
4571            "pkg",
4572            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4573        );
4574        assert!(inner.is_implied_by(&outer));
4575
4576        // pkg <= 2.0 is implied by pkg << 1.5
4577        let outer = Relation::new(
4578            "pkg",
4579            Some((VersionConstraint::LessThan, "1.5".parse().unwrap())),
4580        );
4581        assert!(inner.is_implied_by(&outer));
4582    }
4583
4584    #[test]
4585    fn test_relation_is_implied_by_equal() {
4586        // pkg = 1.0 is only implied by pkg = 1.0
4587        let inner = Relation::new(
4588            "pkg",
4589            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4590        );
4591        let outer = Relation::new(
4592            "pkg",
4593            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4594        );
4595        assert!(inner.is_implied_by(&outer));
4596
4597        // Not implied by different version
4598        let outer = Relation::new(
4599            "pkg",
4600            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4601        );
4602        assert!(!inner.is_implied_by(&outer));
4603
4604        // Not implied by >= constraint
4605        let outer = Relation::new(
4606            "pkg",
4607            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4608        );
4609        assert!(!inner.is_implied_by(&outer));
4610    }
4611
4612    #[test]
4613    fn test_relation_is_implied_by_greater_than() {
4614        // pkg >> 1.0 is implied by pkg >> 2.0
4615        let inner = Relation::new(
4616            "pkg",
4617            Some((VersionConstraint::GreaterThan, "1.0".parse().unwrap())),
4618        );
4619        let outer = Relation::new(
4620            "pkg",
4621            Some((VersionConstraint::GreaterThan, "2.0".parse().unwrap())),
4622        );
4623        assert!(inner.is_implied_by(&outer));
4624
4625        // pkg >> 1.0 is implied by pkg = 2.0
4626        let outer = Relation::new(
4627            "pkg",
4628            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4629        );
4630        assert!(inner.is_implied_by(&outer));
4631
4632        // pkg >> 1.0 is implied by pkg >= 1.5 (strictly greater)
4633        let outer = Relation::new(
4634            "pkg",
4635            Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())),
4636        );
4637        assert!(inner.is_implied_by(&outer));
4638
4639        // pkg >> 1.0 is NOT implied by pkg >= 1.0 (could be equal)
4640        let outer = Relation::new(
4641            "pkg",
4642            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4643        );
4644        assert!(!inner.is_implied_by(&outer));
4645    }
4646
4647    #[test]
4648    fn test_relation_is_implied_by_less_than() {
4649        // pkg << 2.0 is implied by pkg << 1.0
4650        let inner = Relation::new(
4651            "pkg",
4652            Some((VersionConstraint::LessThan, "2.0".parse().unwrap())),
4653        );
4654        let outer = Relation::new(
4655            "pkg",
4656            Some((VersionConstraint::LessThan, "1.0".parse().unwrap())),
4657        );
4658        assert!(inner.is_implied_by(&outer));
4659
4660        // pkg << 2.0 is implied by pkg = 1.0
4661        let outer = Relation::new(
4662            "pkg",
4663            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4664        );
4665        assert!(inner.is_implied_by(&outer));
4666
4667        // pkg << 2.0 is implied by pkg <= 1.5 (strictly less)
4668        let outer = Relation::new(
4669            "pkg",
4670            Some((VersionConstraint::LessThanEqual, "1.5".parse().unwrap())),
4671        );
4672        assert!(inner.is_implied_by(&outer));
4673    }
4674
4675    #[test]
4676    fn test_relation_is_implied_by_incompatible_constraints() {
4677        // >= and <= are incompatible
4678        let inner = Relation::new(
4679            "pkg",
4680            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4681        );
4682        let outer = Relation::new(
4683            "pkg",
4684            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4685        );
4686        assert!(!inner.is_implied_by(&outer));
4687        assert!(!outer.is_implied_by(&inner));
4688    }
4689
4690    #[test]
4691    fn test_entry_is_implied_by_identical() {
4692        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4693        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4694        assert!(inner.is_implied_by(&outer));
4695    }
4696
4697    #[test]
4698    fn test_entry_is_implied_by_or_group() {
4699        // "pkg >= 1.0" is implied by "pkg >= 1.5 | libc6"
4700        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4701        let outer: Entry = "pkg (>= 1.5) | libc6".parse().unwrap();
4702        assert!(inner.is_implied_by(&outer));
4703    }
4704
4705    #[test]
4706    fn test_entry_is_implied_by_simple_or() {
4707        // "pkg1 | pkg2" is implied by "pkg1" (first alternative satisfies)
4708        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4709        let outer: Entry = "pkg1".parse().unwrap();
4710        assert!(inner.is_implied_by(&outer));
4711
4712        // Also implied by "pkg2"
4713        let outer: Entry = "pkg2".parse().unwrap();
4714        assert!(inner.is_implied_by(&outer));
4715    }
4716
4717    #[test]
4718    fn test_entry_is_implied_by_not_implied() {
4719        // "pkg >= 2.0" is NOT implied by "pkg >= 1.0"
4720        let inner: Entry = "pkg (>= 2.0)".parse().unwrap();
4721        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4722        assert!(!inner.is_implied_by(&outer));
4723    }
4724
4725    #[test]
4726    fn test_entry_is_implied_by_different_packages() {
4727        let inner: Entry = "pkg1".parse().unwrap();
4728        let outer: Entry = "pkg2".parse().unwrap();
4729        assert!(!inner.is_implied_by(&outer));
4730    }
4731
4732    #[test]
4733    fn test_entry_is_implied_by_complex_or() {
4734        // "pkg1 | pkg2" is implied by "pkg1 | pkg2" (identical)
4735        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4736        let outer: Entry = "pkg1 | pkg2".parse().unwrap();
4737        assert!(inner.is_implied_by(&outer));
4738
4739        // "pkg1 | pkg2" is implied by "pkg1 | pkg2 | pkg3" (one matches)
4740        let outer: Entry = "pkg1 | pkg2 | pkg3".parse().unwrap();
4741        assert!(inner.is_implied_by(&outer));
4742    }
4743
4744    #[test]
4745    fn test_parse_version_with_epoch() {
4746        // Test parsing version strings with epoch (e.g., "1:2.3.2-2~")
4747        // The colon should be treated as part of the version, not as a delimiter
4748        let input = "amule-dbg (<< 1:2.3.2-2~)";
4749        let parsed: Relations = input.parse().unwrap();
4750        assert_eq!(parsed.to_string(), input);
4751        assert_eq!(parsed.entries().count(), 1);
4752        let entry = parsed.entries().next().unwrap();
4753        assert_eq!(entry.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4754        assert_eq!(entry.relations().count(), 1);
4755        let relation = entry.relations().next().unwrap();
4756        assert_eq!(relation.name(), "amule-dbg");
4757        assert_eq!(relation.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4758        assert_eq!(
4759            relation.version(),
4760            Some((VersionConstraint::LessThan, "1:2.3.2-2~".parse().unwrap()))
4761        );
4762    }
4763
4764    #[test]
4765    fn test_ensure_relation_add_new() {
4766        // Test adding a new relation that doesn't exist yet
4767        let mut relations: Relations = "python3".parse().unwrap();
4768        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4769        let added = relations.ensure_relation(new_entry);
4770        assert!(added);
4771        // debhelper is inserted in sorted position (alphabetically before python3)
4772        assert_eq!(relations.to_string(), "debhelper (>= 12), python3");
4773    }
4774
4775    #[test]
4776    fn test_ensure_relation_already_satisfied() {
4777        // Test that a relation is not added if it's already satisfied by a stronger constraint
4778        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
4779        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4780        let added = relations.ensure_relation(new_entry);
4781        assert!(!added);
4782        assert_eq!(relations.to_string(), "debhelper (>= 13)");
4783    }
4784
4785    #[test]
4786    fn test_ensure_relation_replace_weaker() {
4787        // Test that a weaker relation is replaced with a stronger one
4788        let mut relations: Relations = "debhelper (>= 11)".parse().unwrap();
4789        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4790        let added = relations.ensure_relation(new_entry);
4791        assert!(added);
4792        assert_eq!(relations.to_string(), "debhelper (>= 13)");
4793    }
4794
4795    #[test]
4796    fn test_ensure_relation_replace_multiple_weaker() {
4797        // Test that multiple weaker relations are replaced/removed when a stronger one is added
4798        let mut relations: Relations = "debhelper (>= 11), debhelper (>= 10), python3"
4799            .parse()
4800            .unwrap();
4801        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4802        let added = relations.ensure_relation(new_entry);
4803        assert!(added);
4804        assert_eq!(relations.to_string(), "debhelper (>= 13), python3");
4805    }
4806
4807    #[test]
4808    fn test_ensure_relation_identical_entry() {
4809        // Test that an identical entry is not added again
4810        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
4811        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4812        let added = relations.ensure_relation(new_entry);
4813        assert!(!added);
4814        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4815    }
4816
4817    #[test]
4818    fn test_ensure_relation_no_version_constraint() {
4819        // Test that a relation without version constraint is added
4820        let mut relations: Relations = "python3".parse().unwrap();
4821        let new_entry: Entry = "debhelper".parse().unwrap();
4822        let added = relations.ensure_relation(new_entry);
4823        assert!(added);
4824        // debhelper is inserted in sorted position (alphabetically before python3)
4825        assert_eq!(relations.to_string(), "debhelper, python3");
4826    }
4827
4828    #[test]
4829    fn test_ensure_relation_strengthen_unversioned() {
4830        // Test that a versioned constraint replaces an unversioned one
4831        // An unversioned dependency is weaker than a versioned one
4832        let mut relations: Relations = "debhelper".parse().unwrap();
4833        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4834        let added = relations.ensure_relation(new_entry);
4835        assert!(added);
4836        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4837    }
4838
4839    #[test]
4840    fn test_ensure_relation_versioned_implies_unversioned() {
4841        // Test that an unversioned dependency is already satisfied by a versioned one
4842        // A versioned dependency is stronger and implies the unversioned one
4843        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
4844        let new_entry: Entry = "debhelper".parse().unwrap();
4845        let added = relations.ensure_relation(new_entry);
4846        assert!(!added);
4847        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4848    }
4849
4850    #[test]
4851    fn test_ensure_relation_preserves_whitespace() {
4852        // Test that whitespace is preserved when adding a new relation
4853        let mut relations: Relations = "python3,  rustc".parse().unwrap();
4854        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4855        let added = relations.ensure_relation(new_entry);
4856        assert!(added);
4857        // debhelper is inserted in sorted position (alphabetically before python3 and rustc)
4858        assert_eq!(relations.to_string(), "debhelper (>= 12),  python3,  rustc");
4859    }
4860
4861    #[test]
4862    fn test_ensure_relation_empty_relations() {
4863        // Test adding to empty relations
4864        let mut relations: Relations = Relations::new();
4865        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4866        let added = relations.ensure_relation(new_entry);
4867        assert!(added);
4868        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4869    }
4870
4871    #[test]
4872    fn test_ensure_relation_alternative_dependencies() {
4873        // Test with alternative dependencies (|)
4874        let mut relations: Relations = "python3 | python3-minimal".parse().unwrap();
4875        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4876        let added = relations.ensure_relation(new_entry);
4877        assert!(added);
4878        // debhelper is inserted in sorted position (alphabetically before python3)
4879        assert_eq!(
4880            relations.to_string(),
4881            "debhelper (>= 12), python3 | python3-minimal"
4882        );
4883    }
4884
4885    #[test]
4886    fn test_ensure_relation_replace_in_middle() {
4887        // Test that replacing a weaker entry in the middle preserves order
4888        let mut relations: Relations = "python3, debhelper (>= 11), rustc".parse().unwrap();
4889        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4890        let added = relations.ensure_relation(new_entry);
4891        assert!(added);
4892        assert_eq!(relations.to_string(), "python3, debhelper (>= 13), rustc");
4893    }
4894
4895    #[test]
4896    fn test_ensure_relation_with_different_package() {
4897        // Test that adding a different package doesn't affect existing ones
4898        let mut relations: Relations = "python3, debhelper (>= 12)".parse().unwrap();
4899        let new_entry: Entry = "rustc".parse().unwrap();
4900        let added = relations.ensure_relation(new_entry);
4901        assert!(added);
4902        assert_eq!(relations.to_string(), "python3, debhelper (>= 12), rustc");
4903    }
4904}