Skip to main content

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