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