debian_control/lossless/
relations.rs

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