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