Skip to main content

debian_control/lossless/
relations.rs

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