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.try_name() == other.try_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.try_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().filter_map(|r| r.try_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.try_name().as_deref() != Some(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().filter_map(|r| r.try_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().filter_map(|r| r.try_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 Some(name) = relation.try_name() else {
1522                continue;
1523            };
1524
1525            // Skip items that should be ignored
1526            if sorting_order.ignore(&name) {
1527                continue;
1528            }
1529
1530            // Check if this breaks the sort order
1531            if let Some(ref last) = last_name {
1532                if sorting_order.lt(&name, last) {
1533                    return false;
1534                }
1535            }
1536
1537            last_name = Some(name);
1538        }
1539        true
1540    }
1541
1542    /// Find the position to insert an entry while maintaining sort order.
1543    ///
1544    /// This method detects the current sorting order and returns the appropriate
1545    /// insertion position. If there are fewer than 2 entries, it defaults to
1546    /// WrapAndSortOrder. If no sorting order is detected, it returns the end position.
1547    ///
1548    /// # Arguments
1549    /// * `entry` - The entry to insert
1550    ///
1551    /// # Returns
1552    /// The index where the entry should be inserted
1553    fn find_insert_position(&self, entry: &Entry) -> usize {
1554        // Get the package name from the first relation in the entry
1555        let Some(relation) = entry.relations().next() else {
1556            // Empty entry, just append at the end
1557            return self.len();
1558        };
1559        let Some(package_name) = relation.try_name() else {
1560            return self.len();
1561        };
1562
1563        // Count non-empty entries
1564        let count = self.entries().filter(|e| !e.is_empty()).count();
1565
1566        // If there are less than 2 items, default to WrapAndSortOrder
1567        let sorting_order: Box<dyn SortingOrder> = if count < 2 {
1568            Box::new(WrapAndSortOrder)
1569        } else {
1570            // Try to detect which sorting order is being used
1571            // Try WrapAndSortOrder first, then DefaultSortingOrder
1572            if self.is_sorted(&WrapAndSortOrder) {
1573                Box::new(WrapAndSortOrder)
1574            } else if self.is_sorted(&DefaultSortingOrder) {
1575                Box::new(DefaultSortingOrder)
1576            } else {
1577                // No sorting order detected, just append at the end
1578                return self.len();
1579            }
1580        };
1581
1582        // If adding a special item that should be ignored by this sort order, append at the end
1583        if sorting_order.ignore(&package_name) {
1584            return self.len();
1585        }
1586
1587        // Insert in sorted order among regular items
1588        let mut position = 0;
1589        for (idx, existing_entry) in self.entries().enumerate() {
1590            let mut existing_relations = existing_entry.relations();
1591            let Some(existing_relation) = existing_relations.next() else {
1592                // Empty entry, skip
1593                position += 1;
1594                continue;
1595            };
1596
1597            let Some(existing_name) = existing_relation.try_name() else {
1598                position += 1;
1599                continue;
1600            };
1601
1602            // Skip special items when finding insertion position
1603            if sorting_order.ignore(&existing_name) {
1604                position += 1;
1605                continue;
1606            }
1607
1608            // Compare with regular items only
1609            if sorting_order.lt(&package_name, &existing_name) {
1610                return idx;
1611            }
1612            position += 1;
1613        }
1614
1615        position
1616    }
1617
1618    /// Drop a dependency from the relations by package name.
1619    ///
1620    /// # Arguments
1621    /// * `package` - The package name to remove
1622    ///
1623    /// # Returns
1624    /// `true` if the package was found and removed, `false` otherwise
1625    ///
1626    /// # Example
1627    /// ```
1628    /// use debian_control::lossless::relations::Relations;
1629    ///
1630    /// let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1631    /// assert!(relations.drop_dependency("debhelper"));
1632    /// assert_eq!(relations.to_string(), "python3, rustc");
1633    /// assert!(!relations.drop_dependency("nonexistent"));
1634    /// ```
1635    pub fn drop_dependency(&mut self, package: &str) -> bool {
1636        let indices_to_remove: Vec<_> = self
1637            .entries()
1638            .enumerate()
1639            .filter_map(|(idx, entry)| {
1640                let relations: Vec<_> = entry.relations().collect();
1641                let names: Vec<_> = relations.iter().filter_map(|r| r.try_name()).collect();
1642                if names == vec![package] {
1643                    Some(idx)
1644                } else {
1645                    None
1646                }
1647            })
1648            .collect();
1649
1650        let found = !indices_to_remove.is_empty();
1651
1652        // Remove in reverse order to maintain correct indices
1653        for idx in indices_to_remove.into_iter().rev() {
1654            self.remove_entry(idx);
1655        }
1656
1657        found
1658    }
1659
1660    /// Add a dependency at a specific position or auto-detect the position.
1661    ///
1662    /// If `position` is `None`, the position is automatically determined based
1663    /// on the detected sorting order. If a sorting order is detected, the entry
1664    /// is inserted in the appropriate position to maintain that order. Otherwise,
1665    /// it is appended at the end.
1666    ///
1667    /// # Arguments
1668    /// * `entry` - The entry to add
1669    /// * `position` - Optional position to insert at
1670    ///
1671    /// # Example
1672    /// ```
1673    /// use debian_control::lossless::relations::{Relations, Relation, Entry};
1674    ///
1675    /// let mut relations: Relations = "python3, rustc".parse().unwrap();
1676    /// let entry = Entry::from(Relation::simple("debhelper"));
1677    /// relations.add_dependency(entry, None);
1678    /// // debhelper is inserted in sorted order (if order is detected)
1679    /// ```
1680    pub fn add_dependency(&mut self, entry: Entry, position: Option<usize>) {
1681        let pos = position.unwrap_or_else(|| self.find_insert_position(&entry));
1682        self.insert(pos, entry);
1683    }
1684
1685    /// Get the entry containing a specific package.
1686    ///
1687    /// This returns the first entry that contains exactly one relation with the
1688    /// specified package name (no alternatives).
1689    ///
1690    /// # Arguments
1691    /// * `package` - The package name to search for
1692    ///
1693    /// # Returns
1694    /// A tuple of (index, Entry) if found
1695    ///
1696    /// # Errors
1697    /// Returns `Err` with a message if the package is found in a complex rule (with alternatives)
1698    ///
1699    /// # Example
1700    /// ```
1701    /// use debian_control::lossless::relations::Relations;
1702    ///
1703    /// let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
1704    /// let (idx, entry) = relations.get_relation("debhelper").unwrap();
1705    /// assert_eq!(idx, 1);
1706    /// assert_eq!(entry.to_string(), "debhelper (>= 12)");
1707    /// ```
1708    pub fn get_relation(&self, package: &str) -> Result<(usize, Entry), String> {
1709        for (idx, entry) in self.entries().enumerate() {
1710            let relations: Vec<_> = entry.relations().collect();
1711            let names: Vec<_> = relations.iter().filter_map(|r| r.try_name()).collect();
1712
1713            if names.len() > 1 && names.contains(&package.to_string()) {
1714                return Err(format!("Complex rule for {}, aborting", package));
1715            }
1716
1717            if names.len() == 1 && names[0] == package {
1718                return Ok((idx, entry));
1719            }
1720        }
1721        Err(format!("Package {} not found", package))
1722    }
1723
1724    /// Iterate over all entries containing a specific package.
1725    ///
1726    /// # Arguments
1727    /// * `package` - The package name to search for
1728    ///
1729    /// # Returns
1730    /// An iterator over tuples of (index, Entry)
1731    ///
1732    /// # Example
1733    /// ```
1734    /// use debian_control::lossless::relations::Relations;
1735    ///
1736    /// let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
1737    /// let entries: Vec<_> = relations.iter_relations_for("python3").collect();
1738    /// assert_eq!(entries.len(), 1);
1739    /// ```
1740    pub fn iter_relations_for(&self, package: &str) -> impl Iterator<Item = (usize, Entry)> + '_ {
1741        let package = package.to_string();
1742        self.entries().enumerate().filter(move |(_, entry)| {
1743            let names: Vec<_> = entry.relations().filter_map(|r| r.try_name()).collect();
1744            names.contains(&package)
1745        })
1746    }
1747
1748    /// Check whether a package exists in the relations.
1749    ///
1750    /// # Arguments
1751    /// * `package` - The package name to search for
1752    ///
1753    /// # Returns
1754    /// `true` if the package is found, `false` otherwise
1755    ///
1756    /// # Example
1757    /// ```
1758    /// use debian_control::lossless::relations::Relations;
1759    ///
1760    /// let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1761    /// assert!(relations.has_relation("debhelper"));
1762    /// assert!(!relations.has_relation("nonexistent"));
1763    /// ```
1764    pub fn has_relation(&self, package: &str) -> bool {
1765        self.entries().any(|entry| {
1766            entry
1767                .relations()
1768                .any(|r| r.try_name().as_deref() == Some(package))
1769        })
1770    }
1771}
1772
1773impl From<Vec<Entry>> for Relations {
1774    fn from(entries: Vec<Entry>) -> Self {
1775        let mut builder = GreenNodeBuilder::new();
1776        builder.start_node(ROOT.into());
1777        for (i, entry) in entries.into_iter().enumerate() {
1778            if i > 0 {
1779                builder.token(COMMA.into(), ",");
1780                builder.token(WHITESPACE.into(), " ");
1781            }
1782            inject(&mut builder, entry.0);
1783        }
1784        builder.finish_node();
1785        Relations(SyntaxNode::new_root_mut(builder.finish()))
1786    }
1787}
1788
1789impl From<Entry> for Relations {
1790    fn from(entry: Entry) -> Self {
1791        Self::from(vec![entry])
1792    }
1793}
1794
1795impl Default for Entry {
1796    fn default() -> Self {
1797        Self::new()
1798    }
1799}
1800
1801impl PartialOrd for Entry {
1802    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1803        Some(self.cmp(other))
1804    }
1805}
1806
1807impl Eq for Entry {}
1808
1809impl Ord for Entry {
1810    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1811        let mut rels_a = self.relations();
1812        let mut rels_b = other.relations();
1813        while let (Some(a), Some(b)) = (rels_a.next(), rels_b.next()) {
1814            match a.cmp(&b) {
1815                std::cmp::Ordering::Equal => continue,
1816                x => return x,
1817            }
1818        }
1819
1820        if rels_a.next().is_some() {
1821            return std::cmp::Ordering::Greater;
1822        }
1823
1824        if rels_b.next().is_some() {
1825            return std::cmp::Ordering::Less;
1826        }
1827
1828        std::cmp::Ordering::Equal
1829    }
1830}
1831
1832impl Entry {
1833    /// Create a new entry
1834    pub fn new() -> Self {
1835        let mut builder = GreenNodeBuilder::new();
1836        builder.start_node(SyntaxKind::ENTRY.into());
1837        builder.finish_node();
1838        Entry(SyntaxNode::new_root_mut(builder.finish()))
1839    }
1840
1841    /// Replace the relation at the given index
1842    pub fn replace(&mut self, idx: usize, relation: Relation) {
1843        let current_relation = self.get_relation(idx).unwrap();
1844
1845        let old_root = current_relation.0;
1846        let new_root = relation.0;
1847        // Preserve white the current relation has
1848        let mut prev = new_root.first_child_or_token();
1849        let mut new_head_len = 0;
1850        // First, strip off any whitespace from the new relation
1851        while let Some(p) = prev {
1852            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1853                new_head_len += 1;
1854                prev = p.next_sibling_or_token();
1855            } else {
1856                break;
1857            }
1858        }
1859        let mut new_tail_len = 0;
1860        let mut next = new_root.last_child_or_token();
1861        while let Some(n) = next {
1862            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1863                new_tail_len += 1;
1864                next = n.prev_sibling_or_token();
1865            } else {
1866                break;
1867            }
1868        }
1869        // Then, inherit the whitespace from the old relation
1870        let mut prev = old_root.first_child_or_token();
1871        let mut old_head = vec![];
1872        while let Some(p) = prev {
1873            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1874                old_head.push(p.clone());
1875                prev = p.next_sibling_or_token();
1876            } else {
1877                break;
1878            }
1879        }
1880        let mut old_tail = vec![];
1881        let mut next = old_root.last_child_or_token();
1882        while let Some(n) = next {
1883            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1884                old_tail.push(n.clone());
1885                next = n.prev_sibling_or_token();
1886            } else {
1887                break;
1888            }
1889        }
1890        new_root.splice_children(0..new_head_len, old_head);
1891        let tail_pos = new_root.children_with_tokens().count() - new_tail_len;
1892        new_root.splice_children(
1893            tail_pos - new_tail_len..tail_pos,
1894            old_tail.into_iter().rev(),
1895        );
1896        let index = old_root.index();
1897        self.0
1898            .splice_children(index..index + 1, vec![new_root.into()]);
1899    }
1900
1901    /// Wrap and sort the relations in this entry
1902    #[must_use]
1903    pub fn wrap_and_sort(&self) -> Self {
1904        let mut relations = self
1905            .relations()
1906            .map(|r| r.wrap_and_sort())
1907            .collect::<Vec<_>>();
1908        // TODO: preserve comments
1909        relations.sort();
1910        Self::from(relations)
1911    }
1912
1913    /// Iterate over the relations in this entry
1914    pub fn relations(&self) -> impl Iterator<Item = Relation> + '_ {
1915        self.0.children().filter_map(Relation::cast)
1916    }
1917
1918    /// Iterate over the relations in this entry
1919    pub fn iter(&self) -> impl Iterator<Item = Relation> + '_ {
1920        self.relations()
1921    }
1922
1923    /// Get the relation at the given index
1924    pub fn get_relation(&self, idx: usize) -> Option<Relation> {
1925        self.relations().nth(idx)
1926    }
1927
1928    /// Remove the relation at the given index
1929    ///
1930    /// # Arguments
1931    /// * `idx` - The index of the relation to remove
1932    ///
1933    /// # Example
1934    /// ```
1935    /// use debian_control::lossless::relations::{Relation,Entry};
1936    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-requests".parse().unwrap();
1937    /// entry.remove_relation(1);
1938    /// assert_eq!(entry.to_string(), "python3-dulwich (>= 0.19.0)");
1939    /// ```
1940    pub fn remove_relation(&self, idx: usize) -> Relation {
1941        let mut relation = self.get_relation(idx).unwrap();
1942        relation.remove();
1943        relation
1944    }
1945
1946    /// Check if this entry is satisfied by the given package versions.
1947    ///
1948    /// # Arguments
1949    /// * `package_version` - A function that returns the version of a package.
1950    ///
1951    /// # Example
1952    /// ```
1953    /// use debian_control::lossless::relations::{Relation,Entry};
1954    /// let entry = Entry::from(vec!["samba (>= 2.0)".parse::<Relation>().unwrap()]);
1955    /// assert!(entry.satisfied_by(|name: &str| -> Option<debversion::Version> {
1956    ///    match name {
1957    ///    "samba" => Some("2.0".parse().unwrap()),
1958    ///    _ => None
1959    /// }}));
1960    /// ```
1961    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
1962        self.relations().any(|r| {
1963            let Some(name) = r.try_name() else {
1964                return false;
1965            };
1966            let actual = package_version.lookup_version(name.as_str());
1967            if let Some((vc, version)) = r.version() {
1968                if let Some(actual) = actual {
1969                    match vc {
1970                        VersionConstraint::GreaterThanEqual => *actual >= version,
1971                        VersionConstraint::LessThanEqual => *actual <= version,
1972                        VersionConstraint::Equal => *actual == version,
1973                        VersionConstraint::GreaterThan => *actual > version,
1974                        VersionConstraint::LessThan => *actual < version,
1975                    }
1976                } else {
1977                    false
1978                }
1979            } else {
1980                actual.is_some()
1981            }
1982        })
1983    }
1984
1985    /// Remove this entry
1986    ///
1987    /// # Example
1988    /// ```
1989    /// use debian_control::lossless::relations::{Relations,Entry};
1990    /// let mut relations: Relations = r"python3-dulwich (>= 0.19.0), python3-urllib3 (<< 1.26.0)".parse().unwrap();
1991    /// let mut entry = relations.get_entry(0).unwrap();
1992    /// entry.remove();
1993    /// assert_eq!(relations.to_string(), "python3-urllib3 (<< 1.26.0)");
1994    /// ```
1995    pub fn remove(&mut self) {
1996        let mut removed_comma = false;
1997        let is_first = !self
1998            .0
1999            .siblings(Direction::Prev)
2000            .skip(1)
2001            .any(|n| n.kind() == ENTRY);
2002        while let Some(n) = self.0.next_sibling_or_token() {
2003            if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2004                n.detach();
2005            } else if n.kind() == COMMA {
2006                n.detach();
2007                removed_comma = true;
2008                break;
2009            } else {
2010                panic!("Unexpected node: {:?}", n);
2011            }
2012        }
2013        if !is_first {
2014            while let Some(n) = self.0.prev_sibling_or_token() {
2015                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2016                    n.detach();
2017                } else if !removed_comma && n.kind() == COMMA {
2018                    n.detach();
2019                    break;
2020                } else {
2021                    break;
2022                }
2023            }
2024        } else {
2025            while let Some(n) = self.0.next_sibling_or_token() {
2026                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2027                    n.detach();
2028                } else {
2029                    break;
2030                }
2031            }
2032        }
2033        self.0.detach();
2034    }
2035
2036    /// Check if this entry is empty
2037    pub fn is_empty(&self) -> bool {
2038        self.relations().count() == 0
2039    }
2040
2041    /// Get the number of relations in this entry
2042    pub fn len(&self) -> usize {
2043        self.relations().count()
2044    }
2045
2046    /// Push a new relation to the entry
2047    ///
2048    /// # Arguments
2049    /// * `relation` - The relation to push
2050    ///
2051    /// # Example
2052    /// ```
2053    /// use debian_control::lossless::relations::{Relation,Entry};
2054    /// let mut entry: Entry = "samba (>= 2.0)".parse().unwrap();
2055    /// entry.push("python3-requests".parse().unwrap());
2056    /// assert_eq!(entry.to_string(), "samba (>= 2.0) | python3-requests");
2057    /// ```
2058    pub fn push(&mut self, relation: Relation) {
2059        let is_empty = !self
2060            .0
2061            .children_with_tokens()
2062            .any(|n| n.kind() == PIPE || n.kind() == RELATION);
2063
2064        let (position, new_children) = if let Some(current_relation) = self.relations().last() {
2065            let to_insert: Vec<NodeOrToken<GreenNode, GreenToken>> = if is_empty {
2066                vec![relation.0.green().into()]
2067            } else {
2068                vec![
2069                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
2070                    NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
2071                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
2072                    relation.0.green().into(),
2073                ]
2074            };
2075
2076            (current_relation.0.index() + 1, to_insert)
2077        } else {
2078            let child_count = self.0.children_with_tokens().count();
2079            (
2080                child_count,
2081                if is_empty {
2082                    vec![relation.0.green().into()]
2083                } else {
2084                    vec![
2085                        NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
2086                        NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
2087                        relation.0.green().into(),
2088                    ]
2089                },
2090            )
2091        };
2092
2093        let new_root = SyntaxNode::new_root_mut(
2094            self.0.replace_with(
2095                self.0
2096                    .green()
2097                    .splice_children(position..position, new_children),
2098            ),
2099        );
2100
2101        if let Some(parent) = self.0.parent() {
2102            parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2103            self.0 = parent
2104                .children_with_tokens()
2105                .nth(self.0.index())
2106                .unwrap()
2107                .clone()
2108                .into_node()
2109                .unwrap();
2110        } else {
2111            self.0 = new_root;
2112        }
2113    }
2114
2115    /// Check if this entry (OR-group) is implied by another entry.
2116    ///
2117    /// An entry is implied by another if any of the relations in this entry
2118    /// is implied by any relation in the outer entry. This follows the semantics
2119    /// of OR-groups in Debian dependencies.
2120    ///
2121    /// For example:
2122    /// - `pkg >= 1.0` is implied by `pkg >= 1.5 | libc6` (first relation matches)
2123    /// - `pkg1 | pkg2` is implied by `pkg1` (pkg1 satisfies the requirement)
2124    ///
2125    /// # Arguments
2126    /// * `outer` - The outer entry that may imply this entry
2127    ///
2128    /// # Returns
2129    /// `true` if this entry is implied by `outer`, `false` otherwise
2130    ///
2131    /// # Example
2132    /// ```
2133    /// use debian_control::lossless::relations::Entry;
2134    ///
2135    /// let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
2136    /// let outer: Entry = "pkg (>= 1.5) | libc6".parse().unwrap();
2137    /// assert!(inner.is_implied_by(&outer));
2138    /// ```
2139    pub fn is_implied_by(&self, outer: &Entry) -> bool {
2140        // If entries are identical, they imply each other
2141        if self == outer {
2142            return true;
2143        }
2144
2145        // Check if any relation in inner is implied by any relation in outer
2146        for inner_rel in self.relations() {
2147            for outer_rel in outer.relations() {
2148                if inner_rel.is_implied_by(&outer_rel) {
2149                    return true;
2150                }
2151            }
2152        }
2153
2154        false
2155    }
2156}
2157
2158fn inject(builder: &mut GreenNodeBuilder, node: SyntaxNode) {
2159    builder.start_node(node.kind().into());
2160    for child in node.children_with_tokens() {
2161        match child {
2162            rowan::NodeOrToken::Node(child) => {
2163                inject(builder, child);
2164            }
2165            rowan::NodeOrToken::Token(token) => {
2166                builder.token(token.kind().into(), token.text());
2167            }
2168        }
2169    }
2170    builder.finish_node();
2171}
2172
2173impl From<Vec<Relation>> for Entry {
2174    fn from(relations: Vec<Relation>) -> Self {
2175        let mut builder = GreenNodeBuilder::new();
2176        builder.start_node(SyntaxKind::ENTRY.into());
2177        for (i, relation) in relations.into_iter().enumerate() {
2178            if i > 0 {
2179                builder.token(WHITESPACE.into(), " ");
2180                builder.token(COMMA.into(), "|");
2181                builder.token(WHITESPACE.into(), " ");
2182            }
2183            inject(&mut builder, relation.0);
2184        }
2185        builder.finish_node();
2186        Entry(SyntaxNode::new_root_mut(builder.finish()))
2187    }
2188}
2189
2190impl From<Relation> for Entry {
2191    fn from(relation: Relation) -> Self {
2192        Self::from(vec![relation])
2193    }
2194}
2195
2196/// Helper function to tokenize a version string, handling epochs
2197/// Version strings like "1:2.3.2-2~" need to be split into: IDENT("1"), COLON, IDENT("2.3.2-2~")
2198fn tokenize_version(builder: &mut GreenNodeBuilder, version: &Version) {
2199    let version_str = version.to_string();
2200
2201    // Split on the first colon (if any) to handle epochs
2202    if let Some(colon_pos) = version_str.find(':') {
2203        // Epoch part (before colon)
2204        builder.token(IDENT.into(), &version_str[..colon_pos]);
2205        builder.token(COLON.into(), ":");
2206        // Version part (after colon)
2207        builder.token(IDENT.into(), &version_str[colon_pos + 1..]);
2208    } else {
2209        // No epoch, just a regular version
2210        builder.token(IDENT.into(), version_str.as_str());
2211    }
2212}
2213
2214impl Relation {
2215    /// Create a new relation
2216    ///
2217    /// # Arguments
2218    /// * `name` - The name of the package
2219    /// * `version_constraint` - The version constraint and version to use
2220    ///
2221    /// # Example
2222    /// ```
2223    /// use debian_control::lossless::relations::{Relation};
2224    /// use debian_control::relations::VersionConstraint;
2225    /// let relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2226    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2227    /// ```
2228    pub fn new(name: &str, version_constraint: Option<(VersionConstraint, Version)>) -> Self {
2229        let mut builder = GreenNodeBuilder::new();
2230        builder.start_node(SyntaxKind::RELATION.into());
2231        builder.token(IDENT.into(), name);
2232        if let Some((vc, version)) = version_constraint {
2233            builder.token(WHITESPACE.into(), " ");
2234            builder.start_node(SyntaxKind::VERSION.into());
2235            builder.token(L_PARENS.into(), "(");
2236            builder.start_node(SyntaxKind::CONSTRAINT.into());
2237            for c in vc.to_string().chars() {
2238                builder.token(
2239                    match c {
2240                        '>' => R_ANGLE.into(),
2241                        '<' => L_ANGLE.into(),
2242                        '=' => EQUAL.into(),
2243                        _ => unreachable!(),
2244                    },
2245                    c.to_string().as_str(),
2246                );
2247            }
2248            builder.finish_node();
2249
2250            builder.token(WHITESPACE.into(), " ");
2251
2252            tokenize_version(&mut builder, &version);
2253
2254            builder.token(R_PARENS.into(), ")");
2255
2256            builder.finish_node();
2257        }
2258
2259        builder.finish_node();
2260        Relation(SyntaxNode::new_root_mut(builder.finish()))
2261    }
2262
2263    /// Wrap and sort this relation
2264    ///
2265    /// # Example
2266    /// ```
2267    /// use debian_control::lossless::relations::Relation;
2268    /// let relation = "  samba  (  >= 2.0) ".parse::<Relation>().unwrap();
2269    /// assert_eq!(relation.wrap_and_sort().to_string(), "samba (>= 2.0)");
2270    /// ```
2271    #[must_use]
2272    pub fn wrap_and_sort(&self) -> Self {
2273        let mut builder = GreenNodeBuilder::new();
2274        builder.start_node(SyntaxKind::RELATION.into());
2275        if let Some(name) = self.try_name() {
2276            builder.token(IDENT.into(), name.as_str());
2277        }
2278        if let Some(archqual) = self.archqual() {
2279            builder.token(COLON.into(), ":");
2280            builder.token(IDENT.into(), archqual.as_str());
2281        }
2282        if let Some((vc, version)) = self.version() {
2283            builder.token(WHITESPACE.into(), " ");
2284            builder.start_node(SyntaxKind::VERSION.into());
2285            builder.token(L_PARENS.into(), "(");
2286            builder.start_node(SyntaxKind::CONSTRAINT.into());
2287            builder.token(
2288                match vc {
2289                    VersionConstraint::GreaterThanEqual => R_ANGLE.into(),
2290                    VersionConstraint::LessThanEqual => L_ANGLE.into(),
2291                    VersionConstraint::Equal => EQUAL.into(),
2292                    VersionConstraint::GreaterThan => R_ANGLE.into(),
2293                    VersionConstraint::LessThan => L_ANGLE.into(),
2294                },
2295                vc.to_string().as_str(),
2296            );
2297            builder.finish_node();
2298            builder.token(WHITESPACE.into(), " ");
2299            tokenize_version(&mut builder, &version);
2300            builder.token(R_PARENS.into(), ")");
2301            builder.finish_node();
2302        }
2303        if let Some(architectures) = self.architectures() {
2304            builder.token(WHITESPACE.into(), " ");
2305            builder.start_node(ARCHITECTURES.into());
2306            builder.token(L_BRACKET.into(), "[");
2307            for (i, arch) in architectures.enumerate() {
2308                if i > 0 {
2309                    builder.token(WHITESPACE.into(), " ");
2310                }
2311                builder.token(IDENT.into(), arch.as_str());
2312            }
2313            builder.token(R_BRACKET.into(), "]");
2314            builder.finish_node();
2315        }
2316        for profiles in self.profiles() {
2317            builder.token(WHITESPACE.into(), " ");
2318            builder.start_node(PROFILES.into());
2319            builder.token(L_ANGLE.into(), "<");
2320            for (i, profile) in profiles.into_iter().enumerate() {
2321                if i > 0 {
2322                    builder.token(WHITESPACE.into(), " ");
2323                }
2324                match profile {
2325                    BuildProfile::Disabled(name) => {
2326                        builder.token(NOT.into(), "!");
2327                        builder.token(IDENT.into(), name.as_str());
2328                    }
2329                    BuildProfile::Enabled(name) => {
2330                        builder.token(IDENT.into(), name.as_str());
2331                    }
2332                }
2333            }
2334            builder.token(R_ANGLE.into(), ">");
2335            builder.finish_node();
2336        }
2337        builder.finish_node();
2338        Relation(SyntaxNode::new_root_mut(builder.finish()))
2339    }
2340
2341    /// Create a new simple relation, without any version constraints.
2342    ///
2343    /// # Example
2344    /// ```
2345    /// use debian_control::lossless::relations::Relation;
2346    /// let relation = Relation::simple("samba");
2347    /// assert_eq!(relation.to_string(), "samba");
2348    /// ```
2349    pub fn simple(name: &str) -> Self {
2350        Self::new(name, None)
2351    }
2352
2353    /// Remove the version constraint from the relation.
2354    ///
2355    /// # Example
2356    /// ```
2357    /// use debian_control::lossless::relations::{Relation};
2358    /// use debian_control::relations::VersionConstraint;
2359    /// let mut relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2360    /// relation.drop_constraint();
2361    /// assert_eq!(relation.to_string(), "samba");
2362    /// ```
2363    pub fn drop_constraint(&mut self) -> bool {
2364        let version_token = self.0.children().find(|n| n.kind() == VERSION);
2365        if let Some(version_token) = version_token {
2366            // Remove any whitespace before the version token
2367            while let Some(prev) = version_token.prev_sibling_or_token() {
2368                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2369                    prev.detach();
2370                } else {
2371                    break;
2372                }
2373            }
2374            version_token.detach();
2375            return true;
2376        }
2377
2378        false
2379    }
2380
2381    /// Return the name of the package in the relation, if present.
2382    ///
2383    /// Returns `None` for malformed relations that lack a package name
2384    /// (e.g. when substvars are parsed without substvar support).
2385    ///
2386    /// # Example
2387    /// ```
2388    /// use debian_control::lossless::relations::Relation;
2389    /// let relation = Relation::simple("samba");
2390    /// assert_eq!(relation.try_name(), Some("samba".to_string()));
2391    /// ```
2392    pub fn try_name(&self) -> Option<String> {
2393        self.0
2394            .children_with_tokens()
2395            .find_map(|it| match it {
2396                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2397                _ => None,
2398            })
2399            .map(|token| token.text().to_string())
2400    }
2401
2402    /// Return the name of the package in the relation.
2403    ///
2404    /// # Panics
2405    /// Panics if the relation has no package name (e.g. malformed input).
2406    /// Prefer [`try_name`](Self::try_name) for potentially malformed relations.
2407    ///
2408    /// # Example
2409    /// ```
2410    /// use debian_control::lossless::relations::Relation;
2411    /// let relation = Relation::simple("samba");
2412    /// assert_eq!(relation.name(), "samba");
2413    /// ```
2414    #[deprecated(
2415        since = "0.3.6",
2416        note = "Use try_name() instead, which returns Option<String>"
2417    )]
2418    pub fn name(&self) -> String {
2419        self.try_name().expect("Relation has no package name")
2420    }
2421
2422    /// Return the archqual
2423    ///
2424    /// # Example
2425    /// ```
2426    /// use debian_control::lossless::relations::Relation;
2427    /// let relation: Relation = "samba:any".parse().unwrap();
2428    /// assert_eq!(relation.archqual(), Some("any".to_string()));
2429    /// ```
2430    pub fn archqual(&self) -> Option<String> {
2431        let archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2432        let node = if let Some(archqual) = archqual {
2433            archqual.children_with_tokens().find_map(|it| match it {
2434                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2435                _ => None,
2436            })
2437        } else {
2438            None
2439        };
2440        node.map(|n| n.text().to_string())
2441    }
2442
2443    /// Set the architecture qualifier for this relation.
2444    ///
2445    /// # Example
2446    /// ```
2447    /// use debian_control::lossless::relations::Relation;
2448    /// let mut relation = Relation::simple("samba");
2449    /// relation.set_archqual("any");
2450    /// assert_eq!(relation.to_string(), "samba:any");
2451    /// ```
2452    pub fn set_archqual(&mut self, archqual: &str) {
2453        let mut builder = GreenNodeBuilder::new();
2454        builder.start_node(ARCHQUAL.into());
2455        builder.token(COLON.into(), ":");
2456        builder.token(IDENT.into(), archqual);
2457        builder.finish_node();
2458
2459        let node_archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2460        if let Some(node_archqual) = node_archqual {
2461            self.0.splice_children(
2462                node_archqual.index()..node_archqual.index() + 1,
2463                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2464            );
2465        } else {
2466            let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2467            let idx = if let Some(name_node) = name_node {
2468                name_node.index() + 1
2469            } else {
2470                0
2471            };
2472            self.0.splice_children(
2473                idx..idx,
2474                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2475            );
2476        }
2477    }
2478
2479    /// Return the version constraint and the version it is constrained to.
2480    pub fn version(&self) -> Option<(VersionConstraint, Version)> {
2481        let vc = self.0.children().find(|n| n.kind() == VERSION);
2482        let vc = vc.as_ref()?;
2483        let constraint = vc.children().find(|n| n.kind() == CONSTRAINT);
2484
2485        // Collect all IDENT and COLON tokens to handle versions with epochs (e.g., "1:2.3.2-2~")
2486        let version_str: String = vc
2487            .children_with_tokens()
2488            .filter_map(|it| match it {
2489                SyntaxElement::Token(token) if token.kind() == IDENT || token.kind() == COLON => {
2490                    Some(token.text().to_string())
2491                }
2492                _ => None,
2493            })
2494            .collect();
2495
2496        if let Some(constraint) = constraint {
2497            if !version_str.is_empty() {
2498                let vc: VersionConstraint = constraint.to_string().parse().unwrap();
2499                Some((vc, version_str.parse().unwrap()))
2500            } else {
2501                None
2502            }
2503        } else {
2504            None
2505        }
2506    }
2507
2508    /// Set the version constraint for this relation
2509    ///
2510    /// # Example
2511    /// ```
2512    /// use debian_control::lossless::relations::{Relation};
2513    /// use debian_control::relations::VersionConstraint;
2514    /// let mut relation = Relation::simple("samba");
2515    /// relation.set_version(Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2516    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2517    /// ```
2518    pub fn set_version(&mut self, version_constraint: Option<(VersionConstraint, Version)>) {
2519        let current_version = self.0.children().find(|n| n.kind() == VERSION);
2520        if let Some((vc, version)) = version_constraint {
2521            let mut builder = GreenNodeBuilder::new();
2522            builder.start_node(VERSION.into());
2523            builder.token(L_PARENS.into(), "(");
2524            builder.start_node(CONSTRAINT.into());
2525            match vc {
2526                VersionConstraint::GreaterThanEqual => {
2527                    builder.token(R_ANGLE.into(), ">");
2528                    builder.token(EQUAL.into(), "=");
2529                }
2530                VersionConstraint::LessThanEqual => {
2531                    builder.token(L_ANGLE.into(), "<");
2532                    builder.token(EQUAL.into(), "=");
2533                }
2534                VersionConstraint::Equal => {
2535                    builder.token(EQUAL.into(), "=");
2536                }
2537                VersionConstraint::GreaterThan => {
2538                    builder.token(R_ANGLE.into(), ">");
2539                }
2540                VersionConstraint::LessThan => {
2541                    builder.token(L_ANGLE.into(), "<");
2542                }
2543            }
2544            builder.finish_node(); // CONSTRAINT
2545            builder.token(WHITESPACE.into(), " ");
2546            tokenize_version(&mut builder, &version);
2547            builder.token(R_PARENS.into(), ")");
2548            builder.finish_node(); // VERSION
2549
2550            if let Some(current_version) = current_version {
2551                self.0.splice_children(
2552                    current_version.index()..current_version.index() + 1,
2553                    vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2554                );
2555            } else {
2556                let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2557                let idx = if let Some(name_node) = name_node {
2558                    name_node.index() + 1
2559                } else {
2560                    0
2561                };
2562                let new_children = vec![
2563                    GreenToken::new(WHITESPACE.into(), " ").into(),
2564                    builder.finish().into(),
2565                ];
2566                let new_root = SyntaxNode::new_root_mut(
2567                    self.0.green().splice_children(idx..idx, new_children),
2568                );
2569                if let Some(parent) = self.0.parent() {
2570                    parent
2571                        .splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2572                    self.0 = parent
2573                        .children_with_tokens()
2574                        .nth(self.0.index())
2575                        .unwrap()
2576                        .clone()
2577                        .into_node()
2578                        .unwrap();
2579                } else {
2580                    self.0 = new_root;
2581                }
2582            }
2583        } else if let Some(current_version) = current_version {
2584            // Remove any whitespace before the version token
2585            while let Some(prev) = current_version.prev_sibling_or_token() {
2586                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2587                    prev.detach();
2588                } else {
2589                    break;
2590                }
2591            }
2592            current_version.detach();
2593        }
2594    }
2595
2596    /// Return an iterator over the architectures for this relation
2597    ///
2598    /// # Example
2599    /// ```
2600    /// use debian_control::lossless::relations::Relation;
2601    /// let relation: Relation = "samba [amd64]".parse().unwrap();
2602    /// assert_eq!(relation.architectures().unwrap().collect::<Vec<_>>(), vec!["amd64".to_string()]);
2603    /// ```
2604    pub fn architectures(&self) -> Option<impl Iterator<Item = String> + '_> {
2605        let architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES)?;
2606
2607        Some(architectures.children_with_tokens().filter_map(|node| {
2608            let token = node.as_token()?;
2609            if token.kind() == IDENT {
2610                Some(token.text().to_string())
2611            } else {
2612                None
2613            }
2614        }))
2615    }
2616
2617    /// Returns an iterator over the build profiles for this relation
2618    ///
2619    /// # Example
2620    /// ```
2621    /// use debian_control::lossless::relations::{Relation};
2622    /// use debian_control::relations::{BuildProfile};
2623    /// let relation: Relation = "samba <!nocheck>".parse().unwrap();
2624    /// assert_eq!(relation.profiles().collect::<Vec<_>>(), vec![vec![BuildProfile::Disabled("nocheck".to_string())]]);
2625    /// ```
2626    pub fn profiles(&self) -> impl Iterator<Item = Vec<BuildProfile>> + '_ {
2627        let profiles = self.0.children().filter(|n| n.kind() == PROFILES);
2628
2629        profiles.map(|profile| {
2630            // iterate over nodes separated by whitespace tokens
2631            let mut ret = vec![];
2632            let mut current = vec![];
2633            for token in profile.children_with_tokens() {
2634                match token.kind() {
2635                    WHITESPACE | NEWLINE => {
2636                        if !current.is_empty() {
2637                            ret.push(current.join("").parse::<BuildProfile>().unwrap());
2638                            current = vec![];
2639                        }
2640                    }
2641                    L_ANGLE | R_ANGLE => {}
2642                    _ => {
2643                        current.push(token.to_string());
2644                    }
2645                }
2646            }
2647            if !current.is_empty() {
2648                ret.push(current.concat().parse().unwrap());
2649            }
2650            ret
2651        })
2652    }
2653
2654    /// Remove this relation
2655    ///
2656    /// # Example
2657    /// ```
2658    /// use debian_control::lossless::relations::{Relation,Entry};
2659    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-urllib3 (<< 1.26.0)".parse().unwrap();
2660    /// let mut relation = entry.get_relation(0).unwrap();
2661    /// relation.remove();
2662    /// assert_eq!(entry.to_string(), "python3-urllib3 (<< 1.26.0)");
2663    /// ```
2664    pub fn remove(&mut self) {
2665        let is_first = !self
2666            .0
2667            .siblings(Direction::Prev)
2668            .skip(1)
2669            .any(|n| n.kind() == RELATION);
2670        if !is_first {
2671            // Not the first item in the list. Remove whitespace backwards to the previous
2672            // pipe, the pipe and any whitespace until the previous relation
2673            while let Some(n) = self.0.prev_sibling_or_token() {
2674                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2675                    n.detach();
2676                } else if n.kind() == PIPE {
2677                    n.detach();
2678                    break;
2679                } else {
2680                    break;
2681                }
2682            }
2683            while let Some(n) = self.0.prev_sibling_or_token() {
2684                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2685                    n.detach();
2686                } else {
2687                    break;
2688                }
2689            }
2690        } else {
2691            // First item in the list. Remove whitespace up to the pipe, the pipe and anything
2692            // before the next relation
2693            while let Some(n) = self.0.next_sibling_or_token() {
2694                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2695                    n.detach();
2696                } else if n.kind() == PIPE {
2697                    n.detach();
2698                    break;
2699                } else {
2700                    break;
2701                }
2702            }
2703
2704            while let Some(n) = self.0.next_sibling_or_token() {
2705                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2706                    n.detach();
2707                } else {
2708                    break;
2709                }
2710            }
2711        }
2712        // If this was the last relation in the entry, remove the entire entry
2713        if let Some(mut parent) = self.0.parent().and_then(Entry::cast) {
2714            if parent.is_empty() {
2715                parent.remove();
2716            } else {
2717                self.0.detach();
2718            }
2719        } else {
2720            self.0.detach();
2721        }
2722    }
2723
2724    /// Set the architectures for this relation
2725    ///
2726    /// # Example
2727    /// ```
2728    /// use debian_control::lossless::relations::Relation;
2729    /// let mut relation = Relation::simple("samba");
2730    /// relation.set_architectures(vec!["amd64", "i386"].into_iter());
2731    /// assert_eq!(relation.to_string(), "samba [amd64 i386]");
2732    /// ```
2733    pub fn set_architectures<'a>(&mut self, architectures: impl Iterator<Item = &'a str>) {
2734        let mut builder = GreenNodeBuilder::new();
2735        builder.start_node(ARCHITECTURES.into());
2736        builder.token(L_BRACKET.into(), "[");
2737        for (i, arch) in architectures.enumerate() {
2738            if i > 0 {
2739                builder.token(WHITESPACE.into(), " ");
2740            }
2741            builder.token(IDENT.into(), arch);
2742        }
2743        builder.token(R_BRACKET.into(), "]");
2744        builder.finish_node();
2745
2746        let node_architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES);
2747        if let Some(node_architectures) = node_architectures {
2748            let new_root = SyntaxNode::new_root_mut(builder.finish());
2749            self.0.splice_children(
2750                node_architectures.index()..node_architectures.index() + 1,
2751                vec![new_root.into()],
2752            );
2753        } else {
2754            let profiles = self.0.children().find(|n| n.kind() == PROFILES);
2755            let idx = if let Some(profiles) = profiles {
2756                profiles.index()
2757            } else {
2758                self.0.children_with_tokens().count()
2759            };
2760            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2761                idx..idx,
2762                vec![
2763                    GreenToken::new(WHITESPACE.into(), " ").into(),
2764                    builder.finish().into(),
2765                ],
2766            ));
2767            if let Some(parent) = self.0.parent() {
2768                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2769                self.0 = parent
2770                    .children_with_tokens()
2771                    .nth(self.0.index())
2772                    .unwrap()
2773                    .clone()
2774                    .into_node()
2775                    .unwrap();
2776            } else {
2777                self.0 = new_root;
2778            }
2779        }
2780    }
2781
2782    /// Add a build profile to this relation
2783    ///
2784    /// # Example
2785    /// ```
2786    /// use debian_control::lossless::relations::Relation;
2787    /// use debian_control::relations::BuildProfile;
2788    /// let mut relation = Relation::simple("samba");
2789    /// relation.add_profile(&[BuildProfile::Disabled("nocheck".to_string())]);
2790    /// assert_eq!(relation.to_string(), "samba <!nocheck>");
2791    /// ```
2792    pub fn add_profile(&mut self, profile: &[BuildProfile]) {
2793        let mut builder = GreenNodeBuilder::new();
2794        builder.start_node(PROFILES.into());
2795        builder.token(L_ANGLE.into(), "<");
2796        for (i, profile) in profile.iter().enumerate() {
2797            if i > 0 {
2798                builder.token(WHITESPACE.into(), " ");
2799            }
2800            match profile {
2801                BuildProfile::Disabled(name) => {
2802                    builder.token(NOT.into(), "!");
2803                    builder.token(IDENT.into(), name.as_str());
2804                }
2805                BuildProfile::Enabled(name) => {
2806                    builder.token(IDENT.into(), name.as_str());
2807                }
2808            }
2809        }
2810        builder.token(R_ANGLE.into(), ">");
2811        builder.finish_node();
2812
2813        let node_profiles = self.0.children().find(|n| n.kind() == PROFILES);
2814        if let Some(node_profiles) = node_profiles {
2815            let new_root = SyntaxNode::new_root_mut(builder.finish());
2816            self.0.splice_children(
2817                node_profiles.index()..node_profiles.index() + 1,
2818                vec![new_root.into()],
2819            );
2820        } else {
2821            let idx = self.0.children_with_tokens().count();
2822            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2823                idx..idx,
2824                vec![
2825                    GreenToken::new(WHITESPACE.into(), " ").into(),
2826                    builder.finish().into(),
2827                ],
2828            ));
2829            if let Some(parent) = self.0.parent() {
2830                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2831                self.0 = parent
2832                    .children_with_tokens()
2833                    .nth(self.0.index())
2834                    .unwrap()
2835                    .clone()
2836                    .into_node()
2837                    .unwrap();
2838            } else {
2839                self.0 = new_root;
2840            }
2841        }
2842    }
2843
2844    /// Build a new relation
2845    pub fn build(name: &str) -> RelationBuilder {
2846        RelationBuilder::new(name)
2847    }
2848
2849    /// Check if this relation is implied by another relation.
2850    ///
2851    /// A relation is implied by another if the outer relation is more restrictive
2852    /// or equal to this relation. For example:
2853    /// - `pkg >= 1.0` is implied by `pkg >= 1.5` (outer is more restrictive)
2854    /// - `pkg >= 1.0` is implied by `pkg = 1.5` (outer is more restrictive)
2855    /// - `pkg` (no version) is implied by any versioned constraint on `pkg`
2856    ///
2857    /// # Arguments
2858    /// * `outer` - The outer relation that may imply this relation
2859    ///
2860    /// # Returns
2861    /// `true` if this relation is implied by `outer`, `false` otherwise
2862    ///
2863    /// # Example
2864    /// ```
2865    /// use debian_control::lossless::relations::Relation;
2866    /// use debian_control::relations::VersionConstraint;
2867    ///
2868    /// let inner = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())));
2869    /// let outer = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())));
2870    /// assert!(inner.is_implied_by(&outer));
2871    ///
2872    /// let inner2 = Relation::new("pkg", None);
2873    /// assert!(inner2.is_implied_by(&outer));
2874    /// ```
2875    pub fn is_implied_by(&self, outer: &Relation) -> bool {
2876        if self.try_name() != outer.try_name() {
2877            return false;
2878        }
2879
2880        let inner_version = self.version();
2881        let outer_version = outer.version();
2882
2883        // No version constraint on inner means it's always implied
2884        if inner_version.is_none() {
2885            return true;
2886        }
2887
2888        // If versions are identical, they imply each other
2889        if inner_version == outer_version {
2890            return true;
2891        }
2892
2893        // Inner has version but outer doesn't - not implied
2894        if outer_version.is_none() {
2895            return false;
2896        }
2897
2898        let (inner_constraint, inner_ver) = inner_version.unwrap();
2899        let (outer_constraint, outer_ver) = outer_version.unwrap();
2900
2901        use VersionConstraint::*;
2902        match inner_constraint {
2903            GreaterThanEqual => match outer_constraint {
2904                GreaterThan => outer_ver > inner_ver,
2905                GreaterThanEqual | Equal => outer_ver >= inner_ver,
2906                LessThan | LessThanEqual => false,
2907            },
2908            Equal => match outer_constraint {
2909                Equal => outer_ver == inner_ver,
2910                _ => false,
2911            },
2912            LessThan => match outer_constraint {
2913                LessThan => outer_ver <= inner_ver,
2914                LessThanEqual | Equal => outer_ver < inner_ver,
2915                GreaterThan | GreaterThanEqual => false,
2916            },
2917            LessThanEqual => match outer_constraint {
2918                LessThanEqual | Equal | LessThan => outer_ver <= inner_ver,
2919                GreaterThan | GreaterThanEqual => false,
2920            },
2921            GreaterThan => match outer_constraint {
2922                GreaterThan => outer_ver >= inner_ver,
2923                Equal | GreaterThanEqual => outer_ver > inner_ver,
2924                LessThan | LessThanEqual => false,
2925            },
2926        }
2927    }
2928}
2929
2930/// A builder for creating a `Relation`
2931///
2932/// # Example
2933/// ```
2934/// use debian_control::lossless::relations::{Relation};
2935/// use debian_control::relations::VersionConstraint;
2936/// let relation = Relation::build("samba")
2937///    .version_constraint(VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())
2938///    .archqual("any")
2939///    .architectures(vec!["amd64".to_string(), "i386".to_string()])
2940///    .build();
2941/// assert_eq!(relation.to_string(), "samba:any (>= 2.0) [amd64 i386]");
2942/// ```
2943pub struct RelationBuilder {
2944    name: String,
2945    version_constraint: Option<(VersionConstraint, Version)>,
2946    archqual: Option<String>,
2947    architectures: Option<Vec<String>>,
2948    profiles: Vec<Vec<BuildProfile>>,
2949}
2950
2951impl RelationBuilder {
2952    /// Create a new `RelationBuilder` with the given package name
2953    fn new(name: &str) -> Self {
2954        Self {
2955            name: name.to_string(),
2956            version_constraint: None,
2957            archqual: None,
2958            architectures: None,
2959            profiles: vec![],
2960        }
2961    }
2962
2963    /// Set the version constraint for this relation
2964    pub fn version_constraint(mut self, vc: VersionConstraint, version: Version) -> Self {
2965        self.version_constraint = Some((vc, version));
2966        self
2967    }
2968
2969    /// Set the architecture qualifier for this relation
2970    pub fn archqual(mut self, archqual: &str) -> Self {
2971        self.archqual = Some(archqual.to_string());
2972        self
2973    }
2974
2975    /// Set the architectures for this relation
2976    pub fn architectures(mut self, architectures: Vec<String>) -> Self {
2977        self.architectures = Some(architectures);
2978        self
2979    }
2980
2981    /// Set the build profiles for this relation
2982    pub fn profiles(mut self, profiles: Vec<Vec<BuildProfile>>) -> Self {
2983        self.profiles = profiles;
2984        self
2985    }
2986
2987    /// Add a build profile to this relation
2988    pub fn add_profile(mut self, profile: Vec<BuildProfile>) -> Self {
2989        self.profiles.push(profile);
2990        self
2991    }
2992
2993    /// Build the `Relation`
2994    pub fn build(self) -> Relation {
2995        let mut relation = Relation::new(&self.name, self.version_constraint);
2996        if let Some(archqual) = &self.archqual {
2997            relation.set_archqual(archqual);
2998        }
2999        if let Some(architectures) = &self.architectures {
3000            relation.set_architectures(architectures.iter().map(|s| s.as_str()));
3001        }
3002        for profile in &self.profiles {
3003            relation.add_profile(profile);
3004        }
3005        relation
3006    }
3007}
3008
3009impl PartialOrd for Relation {
3010    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3011        Some(self.cmp(other))
3012    }
3013}
3014
3015impl Eq for Relation {}
3016
3017impl Ord for Relation {
3018    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3019        // Compare by name first, then by version
3020        let name_cmp = self.try_name().cmp(&other.try_name());
3021        if name_cmp != std::cmp::Ordering::Equal {
3022            return name_cmp;
3023        }
3024
3025        let self_version = self.version();
3026        let other_version = other.version();
3027
3028        match (self_version, other_version) {
3029            (Some((self_vc, self_version)), Some((other_vc, other_version))) => {
3030                let vc_cmp = self_vc.cmp(&other_vc);
3031                if vc_cmp != std::cmp::Ordering::Equal {
3032                    return vc_cmp;
3033                }
3034
3035                self_version.cmp(&other_version)
3036            }
3037            (Some(_), None) => std::cmp::Ordering::Greater,
3038            (None, Some(_)) => std::cmp::Ordering::Less,
3039            (None, None) => std::cmp::Ordering::Equal,
3040        }
3041    }
3042}
3043
3044impl std::str::FromStr for Relations {
3045    type Err = String;
3046
3047    fn from_str(s: &str) -> Result<Self, Self::Err> {
3048        let parse = parse(s, false);
3049        if parse.errors.is_empty() {
3050            Ok(parse.root_mut())
3051        } else {
3052            Err(parse.errors.join("\n"))
3053        }
3054    }
3055}
3056
3057impl std::str::FromStr for Entry {
3058    type Err = String;
3059
3060    fn from_str(s: &str) -> Result<Self, Self::Err> {
3061        let root: Relations = s.parse()?;
3062
3063        let mut entries = root.entries();
3064        let entry = if let Some(entry) = entries.next() {
3065            entry
3066        } else {
3067            return Err("No entry found".to_string());
3068        };
3069
3070        if entries.next().is_some() {
3071            return Err("Multiple entries found".to_string());
3072        }
3073
3074        Ok(entry)
3075    }
3076}
3077
3078impl std::str::FromStr for Relation {
3079    type Err = String;
3080
3081    fn from_str(s: &str) -> Result<Self, Self::Err> {
3082        let entry: Entry = s.parse()?;
3083
3084        let mut relations = entry.relations();
3085        let relation = if let Some(relation) = relations.next() {
3086            relation
3087        } else {
3088            return Err("No relation found".to_string());
3089        };
3090
3091        if relations.next().is_some() {
3092            return Err("Multiple relations found".to_string());
3093        }
3094
3095        Ok(relation)
3096    }
3097}
3098
3099impl From<crate::lossy::Relation> for Relation {
3100    fn from(relation: crate::lossy::Relation) -> Self {
3101        let mut builder = Relation::build(&relation.name);
3102
3103        if let Some((vc, version)) = relation.version {
3104            builder = builder.version_constraint(vc, version);
3105        }
3106
3107        if let Some(archqual) = relation.archqual {
3108            builder = builder.archqual(&archqual);
3109        }
3110
3111        if let Some(architectures) = relation.architectures {
3112            builder = builder.architectures(architectures);
3113        }
3114
3115        builder = builder.profiles(relation.profiles);
3116
3117        builder.build()
3118    }
3119}
3120
3121impl From<Relation> for crate::lossy::Relation {
3122    fn from(relation: Relation) -> Self {
3123        crate::lossy::Relation {
3124            name: relation.try_name().unwrap_or_default(),
3125            version: relation.version(),
3126            archqual: relation.archqual(),
3127            architectures: relation.architectures().map(|a| a.collect()),
3128            profiles: relation.profiles().collect(),
3129        }
3130    }
3131}
3132
3133impl From<Entry> for Vec<crate::lossy::Relation> {
3134    fn from(entry: Entry) -> Self {
3135        entry.relations().map(|r| r.into()).collect()
3136    }
3137}
3138
3139impl From<Vec<crate::lossy::Relation>> for Entry {
3140    fn from(relations: Vec<crate::lossy::Relation>) -> Self {
3141        let relations: Vec<Relation> = relations.into_iter().map(|r| r.into()).collect();
3142        Entry::from(relations)
3143    }
3144}
3145
3146#[cfg(test)]
3147mod tests {
3148    use super::*;
3149
3150    #[test]
3151    fn test_parse() {
3152        let input = "python3-dulwich";
3153        let parsed: Relations = input.parse().unwrap();
3154        assert_eq!(parsed.to_string(), input);
3155        assert_eq!(parsed.entries().count(), 1);
3156        let entry = parsed.entries().next().unwrap();
3157        assert_eq!(entry.to_string(), "python3-dulwich");
3158        assert_eq!(entry.relations().count(), 1);
3159        let relation = entry.relations().next().unwrap();
3160        assert_eq!(relation.to_string(), "python3-dulwich");
3161        assert_eq!(relation.version(), None);
3162
3163        let input = "python3-dulwich (>= 0.20.21)";
3164        let parsed: Relations = input.parse().unwrap();
3165        assert_eq!(parsed.to_string(), input);
3166        assert_eq!(parsed.entries().count(), 1);
3167        let entry = parsed.entries().next().unwrap();
3168        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3169        assert_eq!(entry.relations().count(), 1);
3170        let relation = entry.relations().next().unwrap();
3171        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3172        assert_eq!(
3173            relation.version(),
3174            Some((
3175                VersionConstraint::GreaterThanEqual,
3176                "0.20.21".parse().unwrap()
3177            ))
3178        );
3179    }
3180
3181    #[test]
3182    fn test_multiple() {
3183        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
3184        let parsed: Relations = input.parse().unwrap();
3185        assert_eq!(parsed.to_string(), input);
3186        assert_eq!(parsed.entries().count(), 2);
3187        let entry = parsed.entries().next().unwrap();
3188        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3189        assert_eq!(entry.relations().count(), 1);
3190        let relation = entry.relations().next().unwrap();
3191        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3192        assert_eq!(
3193            relation.version(),
3194            Some((
3195                VersionConstraint::GreaterThanEqual,
3196                "0.20.21".parse().unwrap()
3197            ))
3198        );
3199        let entry = parsed.entries().nth(1).unwrap();
3200        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.21)");
3201        assert_eq!(entry.relations().count(), 1);
3202        let relation = entry.relations().next().unwrap();
3203        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
3204        assert_eq!(
3205            relation.version(),
3206            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
3207        );
3208    }
3209
3210    #[test]
3211    fn test_architectures() {
3212        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
3213        let parsed: Relations = input.parse().unwrap();
3214        assert_eq!(parsed.to_string(), input);
3215        assert_eq!(parsed.entries().count(), 1);
3216        let entry = parsed.entries().next().unwrap();
3217        assert_eq!(
3218            entry.to_string(),
3219            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3220        );
3221        assert_eq!(entry.relations().count(), 1);
3222        let relation = entry.relations().next().unwrap();
3223        assert_eq!(
3224            relation.to_string(),
3225            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3226        );
3227        assert_eq!(relation.version(), None);
3228        assert_eq!(
3229            relation.architectures().unwrap().collect::<Vec<_>>(),
3230            vec![
3231                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
3232            ]
3233            .into_iter()
3234            .map(|s| s.to_string())
3235            .collect::<Vec<_>>()
3236        );
3237    }
3238
3239    #[test]
3240    fn test_profiles() {
3241        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
3242        let parsed: Relations = input.parse().unwrap();
3243        assert_eq!(parsed.to_string(), input);
3244        assert_eq!(parsed.entries().count(), 2);
3245        let entry = parsed.entries().next().unwrap();
3246        assert_eq!(
3247            entry.to_string(),
3248            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3249        );
3250        assert_eq!(entry.relations().count(), 1);
3251        let relation = entry.relations().next().unwrap();
3252        assert_eq!(
3253            relation.to_string(),
3254            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3255        );
3256        assert_eq!(
3257            relation.version(),
3258            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
3259        );
3260        assert_eq!(
3261            relation.architectures().unwrap().collect::<Vec<_>>(),
3262            vec!["i386", "arm"]
3263                .into_iter()
3264                .map(|s| s.to_string())
3265                .collect::<Vec<_>>()
3266        );
3267        assert_eq!(
3268            relation.profiles().collect::<Vec<_>>(),
3269            vec![
3270                vec![BuildProfile::Disabled("nocheck".to_string())],
3271                vec![BuildProfile::Disabled("cross".to_string())]
3272            ]
3273        );
3274    }
3275
3276    #[test]
3277    fn test_substvar() {
3278        let input = "${shlibs:Depends}";
3279
3280        let (parsed, errors) = Relations::parse_relaxed(input, true);
3281        assert_eq!(errors, Vec::<String>::new());
3282        assert_eq!(parsed.to_string(), input);
3283        assert_eq!(parsed.entries().count(), 0);
3284
3285        assert_eq!(
3286            parsed.substvars().collect::<Vec<_>>(),
3287            vec!["${shlibs:Depends}"]
3288        );
3289    }
3290
3291    #[test]
3292    fn test_new() {
3293        let r = Relation::new(
3294            "samba",
3295            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3296        );
3297
3298        assert_eq!(r.to_string(), "samba (>= 2.0)");
3299    }
3300
3301    #[test]
3302    fn test_drop_constraint() {
3303        let mut r = Relation::new(
3304            "samba",
3305            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3306        );
3307
3308        r.drop_constraint();
3309
3310        assert_eq!(r.to_string(), "samba");
3311    }
3312
3313    #[test]
3314    fn test_simple() {
3315        let r = Relation::simple("samba");
3316
3317        assert_eq!(r.to_string(), "samba");
3318    }
3319
3320    #[test]
3321    fn test_remove_first_entry() {
3322        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3323            .parse()
3324            .unwrap();
3325        let removed = rels.remove_entry(0);
3326        assert_eq!(removed.to_string(), "python3-dulwich (>= 0.20.21)");
3327        assert_eq!(rels.to_string(), "python3-dulwich (<< 0.21)");
3328    }
3329
3330    #[test]
3331    fn test_remove_last_entry() {
3332        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3333            .parse()
3334            .unwrap();
3335        rels.remove_entry(1);
3336        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3337    }
3338
3339    #[test]
3340    fn test_remove_middle() {
3341        let mut rels: Relations =
3342            r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21), python3-dulwich (<< 0.22)"#
3343                .parse()
3344                .unwrap();
3345        rels.remove_entry(1);
3346        assert_eq!(
3347            rels.to_string(),
3348            "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.22)"
3349        );
3350    }
3351
3352    #[test]
3353    fn test_remove_added() {
3354        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3355        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3356        rels.push(entry);
3357        rels.remove_entry(1);
3358        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3359    }
3360
3361    #[test]
3362    fn test_push() {
3363        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3364        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3365        rels.push(entry);
3366        assert_eq!(
3367            rels.to_string(),
3368            "python3-dulwich (>= 0.20.21), python3-dulwich"
3369        );
3370    }
3371
3372    #[test]
3373    fn test_insert_with_custom_separator() {
3374        let mut rels: Relations = "python3".parse().unwrap();
3375        let entry = Entry::from(vec![Relation::simple("debhelper")]);
3376        rels.insert_with_separator(1, entry, Some("\n "));
3377        assert_eq!(rels.to_string(), "python3,\n debhelper");
3378    }
3379
3380    #[test]
3381    fn test_insert_with_wrap_and_sort_separator() {
3382        let mut rels: Relations = "python3".parse().unwrap();
3383        let entry = Entry::from(vec![Relation::simple("rustc")]);
3384        // Simulate wrap-and-sort -a style with field name "Depends: " (9 chars)
3385        rels.insert_with_separator(1, entry, Some("\n         "));
3386        assert_eq!(rels.to_string(), "python3,\n         rustc");
3387    }
3388
3389    #[test]
3390    fn test_push_from_empty() {
3391        let mut rels: Relations = "".parse().unwrap();
3392        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3393        rels.push(entry);
3394        assert_eq!(rels.to_string(), "python3-dulwich");
3395    }
3396
3397    #[test]
3398    fn test_insert() {
3399        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3400            .parse()
3401            .unwrap();
3402        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3403        rels.insert(1, entry);
3404        assert_eq!(
3405            rels.to_string(),
3406            "python3-dulwich (>= 0.20.21), python3-dulwich, python3-dulwich (<< 0.21)"
3407        );
3408    }
3409
3410    #[test]
3411    fn test_insert_at_start() {
3412        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3413            .parse()
3414            .unwrap();
3415        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3416        rels.insert(0, entry);
3417        assert_eq!(
3418            rels.to_string(),
3419            "python3-dulwich, python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3420        );
3421    }
3422
3423    #[test]
3424    fn test_insert_after_error() {
3425        let (mut rels, errors) = Relations::parse_relaxed("@foo@, debhelper (>= 1.0)", false);
3426        assert_eq!(
3427            errors,
3428            vec![
3429                "expected $ or identifier but got ERROR",
3430                "expected comma or end of file but got Some(IDENT)",
3431                "expected $ or identifier but got ERROR"
3432            ]
3433        );
3434        let entry = Entry::from(vec![Relation::simple("bar")]);
3435        rels.push(entry);
3436        assert_eq!(rels.to_string(), "@foo@, debhelper (>= 1.0), bar");
3437    }
3438
3439    #[test]
3440    fn test_insert_before_error() {
3441        let (mut rels, errors) = Relations::parse_relaxed("debhelper (>= 1.0), @foo@, bla", false);
3442        assert_eq!(
3443            errors,
3444            vec![
3445                "expected $ or identifier but got ERROR",
3446                "expected comma or end of file but got Some(IDENT)",
3447                "expected $ or identifier but got ERROR"
3448            ]
3449        );
3450        let entry = Entry::from(vec![Relation::simple("bar")]);
3451        rels.insert(0, entry);
3452        assert_eq!(rels.to_string(), "bar, debhelper (>= 1.0), @foo@, bla");
3453    }
3454
3455    #[test]
3456    fn test_replace() {
3457        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3458            .parse()
3459            .unwrap();
3460        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3461        rels.replace(1, entry);
3462        assert_eq!(
3463            rels.to_string(),
3464            "python3-dulwich (>= 0.20.21), python3-dulwich"
3465        );
3466    }
3467
3468    #[test]
3469    fn test_relation_from_entries() {
3470        let entries = vec![
3471            Entry::from(vec![Relation::simple("python3-dulwich")]),
3472            Entry::from(vec![Relation::simple("python3-breezy")]),
3473        ];
3474        let rels: Relations = entries.into();
3475        assert_eq!(rels.entries().count(), 2);
3476        assert_eq!(rels.to_string(), "python3-dulwich, python3-breezy");
3477    }
3478
3479    #[test]
3480    fn test_entry_from_relations() {
3481        let relations = vec![
3482            Relation::simple("python3-dulwich"),
3483            Relation::simple("python3-breezy"),
3484        ];
3485        let entry: Entry = relations.into();
3486        assert_eq!(entry.relations().count(), 2);
3487        assert_eq!(entry.to_string(), "python3-dulwich | python3-breezy");
3488    }
3489
3490    #[test]
3491    fn test_parse_entry() {
3492        let parsed: Entry = "python3-dulwich (>= 0.20.21) | bar".parse().unwrap();
3493        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21) | bar");
3494        assert_eq!(parsed.relations().count(), 2);
3495
3496        assert_eq!(
3497            "foo, bar".parse::<Entry>().unwrap_err(),
3498            "Multiple entries found"
3499        );
3500        assert_eq!("".parse::<Entry>().unwrap_err(), "No entry found");
3501    }
3502
3503    #[test]
3504    fn test_parse_relation() {
3505        let parsed: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3506        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
3507        assert_eq!(
3508            parsed.version(),
3509            Some((
3510                VersionConstraint::GreaterThanEqual,
3511                "0.20.21".parse().unwrap()
3512            ))
3513        );
3514        assert_eq!(
3515            "foo | bar".parse::<Relation>().unwrap_err(),
3516            "Multiple relations found"
3517        );
3518        assert_eq!("".parse::<Relation>().unwrap_err(), "No entry found");
3519    }
3520
3521    #[test]
3522    fn test_special() {
3523        let parsed: Relation = "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3524            .parse()
3525            .unwrap();
3526        assert_eq!(
3527            parsed.to_string(),
3528            "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3529        );
3530        assert_eq!(
3531            parsed.version(),
3532            Some((
3533                VersionConstraint::GreaterThanEqual,
3534                "0.1.138-~~".parse().unwrap()
3535            ))
3536        );
3537        assert_eq!(parsed.archqual(), Some("amd64".to_string()));
3538        assert_eq!(parsed.name(), "librust-breezyshim+dirty-tracker-dev");
3539    }
3540
3541    #[test]
3542    fn test_relations_satisfied_by() {
3543        let rels: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3544            .parse()
3545            .unwrap();
3546        let satisfied = |name: &str| -> Option<debversion::Version> {
3547            match name {
3548                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3549                _ => None,
3550            }
3551        };
3552        assert!(rels.satisfied_by(satisfied));
3553
3554        let satisfied = |name: &str| match name {
3555            "python3-dulwich" => Some("0.21".parse().unwrap()),
3556            _ => None,
3557        };
3558        assert!(!rels.satisfied_by(satisfied));
3559
3560        let satisfied = |name: &str| match name {
3561            "python3-dulwich" => Some("0.20.20".parse().unwrap()),
3562            _ => None,
3563        };
3564        assert!(!rels.satisfied_by(satisfied));
3565    }
3566
3567    #[test]
3568    fn test_entry_satisfied_by() {
3569        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3570            .parse()
3571            .unwrap();
3572        let satisfied = |name: &str| -> Option<debversion::Version> {
3573            match name {
3574                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3575                _ => None,
3576            }
3577        };
3578        assert!(entry.satisfied_by(satisfied));
3579        let satisfied = |name: &str| -> Option<debversion::Version> {
3580            match name {
3581                "python3-dulwich" => Some("0.18".parse().unwrap()),
3582                _ => None,
3583            }
3584        };
3585        assert!(!entry.satisfied_by(satisfied));
3586    }
3587
3588    #[test]
3589    fn test_wrap_and_sort_relation() {
3590        let relation: Relation = "   python3-dulwich   (>= 11) [  amd64 ] <  lala>"
3591            .parse()
3592            .unwrap();
3593
3594        let wrapped = relation.wrap_and_sort();
3595
3596        assert_eq!(
3597            wrapped.to_string(),
3598            "python3-dulwich (>= 11) [amd64] <lala>"
3599        );
3600    }
3601
3602    #[test]
3603    fn test_wrap_and_sort_relations() {
3604        let entry: Relations =
3605            "python3-dulwich (>= 0.20.21)   | bar, \n\n\n\npython3-dulwich (<< 0.21)"
3606                .parse()
3607                .unwrap();
3608
3609        let wrapped = entry.wrap_and_sort();
3610
3611        assert_eq!(
3612            wrapped.to_string(),
3613            "bar | python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3614        );
3615    }
3616
3617    #[cfg(feature = "serde")]
3618    #[test]
3619    fn test_serialize_relations() {
3620        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3621            .parse()
3622            .unwrap();
3623        let serialized = serde_json::to_string(&relations).unwrap();
3624        assert_eq!(
3625            serialized,
3626            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
3627        );
3628    }
3629
3630    #[cfg(feature = "serde")]
3631    #[test]
3632    fn test_deserialize_relations() {
3633        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3634            .parse()
3635            .unwrap();
3636        let serialized = serde_json::to_string(&relations).unwrap();
3637        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
3638        assert_eq!(deserialized.to_string(), relations.to_string());
3639    }
3640
3641    #[cfg(feature = "serde")]
3642    #[test]
3643    fn test_serialize_relation() {
3644        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3645        let serialized = serde_json::to_string(&relation).unwrap();
3646        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
3647    }
3648
3649    #[cfg(feature = "serde")]
3650    #[test]
3651    fn test_deserialize_relation() {
3652        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3653        let serialized = serde_json::to_string(&relation).unwrap();
3654        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
3655        assert_eq!(deserialized.to_string(), relation.to_string());
3656    }
3657
3658    #[cfg(feature = "serde")]
3659    #[test]
3660    fn test_serialize_entry() {
3661        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3662            .parse()
3663            .unwrap();
3664        let serialized = serde_json::to_string(&entry).unwrap();
3665        assert_eq!(
3666            serialized,
3667            r#""python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)""#
3668        );
3669    }
3670
3671    #[cfg(feature = "serde")]
3672    #[test]
3673    fn test_deserialize_entry() {
3674        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3675            .parse()
3676            .unwrap();
3677        let serialized = serde_json::to_string(&entry).unwrap();
3678        let deserialized: Entry = serde_json::from_str(&serialized).unwrap();
3679        assert_eq!(deserialized.to_string(), entry.to_string());
3680    }
3681
3682    #[test]
3683    fn test_remove_first_relation() {
3684        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3685            .parse()
3686            .unwrap();
3687        let mut rel = entry.relations().next().unwrap();
3688        rel.remove();
3689        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.18)");
3690    }
3691
3692    #[test]
3693    fn test_remove_last_relation() {
3694        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3695            .parse()
3696            .unwrap();
3697        let mut rel = entry.relations().nth(1).unwrap();
3698        rel.remove();
3699        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3700    }
3701
3702    #[test]
3703    fn test_remove_only_relation() {
3704        let entry: Entry = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3705        let mut rel = entry.relations().next().unwrap();
3706        rel.remove();
3707        assert_eq!(entry.to_string(), "");
3708    }
3709
3710    #[test]
3711    fn test_relations_is_empty() {
3712        let entry: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3713        assert!(!entry.is_empty());
3714        assert_eq!(1, entry.len());
3715        let mut rel = entry.entries().next().unwrap();
3716        rel.remove();
3717        assert!(entry.is_empty());
3718        assert_eq!(0, entry.len());
3719    }
3720
3721    #[test]
3722    fn test_entry_is_empty() {
3723        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3724            .parse()
3725            .unwrap();
3726        assert!(!entry.is_empty());
3727        assert_eq!(2, entry.len());
3728        let mut rel = entry.relations().next().unwrap();
3729        rel.remove();
3730        assert!(!entry.is_empty());
3731        assert_eq!(1, entry.len());
3732        let mut rel = entry.relations().next().unwrap();
3733        rel.remove();
3734        assert!(entry.is_empty());
3735        assert_eq!(0, entry.len());
3736    }
3737
3738    #[test]
3739    fn test_relation_set_version() {
3740        let mut rel: Relation = "samba".parse().unwrap();
3741        rel.set_version(None);
3742        assert_eq!("samba", rel.to_string());
3743        rel.set_version(Some((
3744            VersionConstraint::GreaterThanEqual,
3745            "2.0".parse().unwrap(),
3746        )));
3747        assert_eq!("samba (>= 2.0)", rel.to_string());
3748        rel.set_version(None);
3749        assert_eq!("samba", rel.to_string());
3750        rel.set_version(Some((
3751            VersionConstraint::GreaterThanEqual,
3752            "2.0".parse().unwrap(),
3753        )));
3754        rel.set_version(Some((
3755            VersionConstraint::GreaterThanEqual,
3756            "1.1".parse().unwrap(),
3757        )));
3758        assert_eq!("samba (>= 1.1)", rel.to_string());
3759    }
3760
3761    #[test]
3762    fn test_replace_relation() {
3763        let mut entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3764            .parse()
3765            .unwrap();
3766        let new_rel = Relation::simple("python3-breezy");
3767        entry.replace(0, new_rel);
3768        assert_eq!(
3769            entry.to_string(),
3770            "python3-breezy | python3-dulwich (<< 0.18)"
3771        );
3772    }
3773
3774    #[test]
3775    fn test_entry_push_relation() {
3776        let relations: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3777        let new_rel = Relation::simple("python3-breezy");
3778        let mut entry = relations.entries().next().unwrap();
3779        entry.push(new_rel);
3780        assert_eq!(
3781            entry.to_string(),
3782            "python3-dulwich (>= 0.20.21) | python3-breezy"
3783        );
3784        assert_eq!(
3785            relations.to_string(),
3786            "python3-dulwich (>= 0.20.21) | python3-breezy"
3787        );
3788    }
3789
3790    #[test]
3791    fn test_relations_remove_empty_entry() {
3792        let (mut relations, errors) = Relations::parse_relaxed("foo, , bar, ", false);
3793        assert_eq!(errors, Vec::<String>::new());
3794        assert_eq!(relations.to_string(), "foo, , bar, ");
3795        assert_eq!(relations.len(), 2);
3796        assert_eq!(
3797            relations.entries().next().unwrap().to_string(),
3798            "foo".to_string()
3799        );
3800        assert_eq!(
3801            relations.entries().nth(1).unwrap().to_string(),
3802            "bar".to_string()
3803        );
3804        relations.remove_entry(1);
3805        assert_eq!(relations.to_string(), "foo, , ");
3806    }
3807
3808    #[test]
3809    fn test_entry_remove_relation() {
3810        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3811        let removed = entry.remove_relation(0);
3812        assert_eq!(removed.to_string(), "python3-dulwich");
3813        assert_eq!(entry.to_string(), "samba");
3814    }
3815
3816    #[test]
3817    fn test_wrap_and_sort_removes_empty_entries() {
3818        let relations: Relations = "foo, , bar, ".parse().unwrap();
3819        let wrapped = relations.wrap_and_sort();
3820        assert_eq!(wrapped.to_string(), "bar, foo");
3821    }
3822
3823    #[test]
3824    fn test_set_archqual() {
3825        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3826        let mut rel = entry.relations().next().unwrap();
3827        rel.set_archqual("amd64");
3828        assert_eq!(rel.to_string(), "python3-dulwich:amd64");
3829        assert_eq!(rel.archqual(), Some("amd64".to_string()));
3830        assert_eq!(entry.to_string(), "python3-dulwich:amd64 | samba");
3831        rel.set_archqual("i386");
3832        assert_eq!(rel.to_string(), "python3-dulwich:i386");
3833        assert_eq!(rel.archqual(), Some("i386".to_string()));
3834        assert_eq!(entry.to_string(), "python3-dulwich:i386 | samba");
3835    }
3836
3837    #[test]
3838    fn test_set_architectures() {
3839        let mut relation = Relation::simple("samba");
3840        relation.set_architectures(vec!["amd64", "i386"].into_iter());
3841        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3842    }
3843
3844    #[test]
3845    fn test_relation_builder_no_architectures() {
3846        // Test that building a relation without architectures doesn't add empty brackets
3847        let relation = Relation::build("debhelper").build();
3848        assert_eq!(relation.to_string(), "debhelper");
3849    }
3850
3851    #[test]
3852    fn test_relation_builder_with_architectures() {
3853        // Test that building a relation with architectures works correctly
3854        let relation = Relation::build("samba")
3855            .architectures(vec!["amd64".to_string(), "i386".to_string()])
3856            .build();
3857        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3858    }
3859
3860    #[test]
3861    fn test_ensure_minimum_version_add_new() {
3862        let mut relations: Relations = "python3".parse().unwrap();
3863        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3864        assert_eq!(relations.to_string(), "python3, debhelper (>= 12)");
3865    }
3866
3867    #[test]
3868    fn test_ensure_minimum_version_update() {
3869        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3870        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3871        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3872    }
3873
3874    #[test]
3875    fn test_ensure_minimum_version_no_change() {
3876        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
3877        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3878        assert_eq!(relations.to_string(), "debhelper (>= 13)");
3879    }
3880
3881    #[test]
3882    fn test_ensure_minimum_version_no_version() {
3883        let mut relations: Relations = "debhelper".parse().unwrap();
3884        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3885        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3886    }
3887
3888    #[test]
3889    fn test_ensure_minimum_version_preserves_newline() {
3890        // Test that newline after the field name is preserved
3891        // This is the format often used in Debian control files:
3892        // Build-Depends:
3893        //  debhelper (>= 9),
3894        //  pkg-config
3895        let input = "\n debhelper (>= 9),\n pkg-config,\n uuid-dev";
3896        let mut relations: Relations = input.parse().unwrap();
3897        relations.ensure_minimum_version("debhelper", &"12~".parse().unwrap());
3898        let result = relations.to_string();
3899
3900        // The newline before the first entry should be preserved
3901        assert!(
3902            result.starts_with('\n'),
3903            "Expected result to start with newline, got: {:?}",
3904            result
3905        );
3906        assert_eq!(result, "\n debhelper (>= 12~),\n pkg-config,\n uuid-dev");
3907    }
3908
3909    #[test]
3910    fn test_ensure_minimum_version_preserves_newline_in_control() {
3911        // Test the full scenario from the bug report
3912        use crate::lossless::Control;
3913        use std::str::FromStr;
3914
3915        let input = r#"Source: f2fs-tools
3916Section: admin
3917Priority: optional
3918Maintainer: Test <test@example.com>
3919Build-Depends:
3920 debhelper (>= 9),
3921 pkg-config,
3922 uuid-dev
3923
3924Package: f2fs-tools
3925Description: test
3926"#;
3927
3928        let control = Control::from_str(input).unwrap();
3929        let mut source = control.source().unwrap();
3930        let mut build_depends = source.build_depends().unwrap();
3931
3932        let version = Version::from_str("12~").unwrap();
3933        build_depends.ensure_minimum_version("debhelper", &version);
3934
3935        source.set_build_depends(&build_depends);
3936
3937        let output = control.to_string();
3938
3939        // Check that the formatting is preserved - the newline after "Build-Depends:" should still be there
3940        assert!(
3941            output.contains("Build-Depends:\n debhelper (>= 12~)"),
3942            "Expected 'Build-Depends:\\n debhelper (>= 12~)' but got:\n{}",
3943            output
3944        );
3945    }
3946
3947    #[test]
3948    fn test_ensure_exact_version_add_new() {
3949        let mut relations: Relations = "python3".parse().unwrap();
3950        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3951        assert_eq!(relations.to_string(), "python3, debhelper (= 12)");
3952    }
3953
3954    #[test]
3955    fn test_ensure_exact_version_update() {
3956        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3957        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3958        assert_eq!(relations.to_string(), "debhelper (= 12)");
3959    }
3960
3961    #[test]
3962    fn test_ensure_exact_version_no_change() {
3963        let mut relations: Relations = "debhelper (= 12)".parse().unwrap();
3964        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3965        assert_eq!(relations.to_string(), "debhelper (= 12)");
3966    }
3967
3968    #[test]
3969    fn test_ensure_some_version_add_new() {
3970        let mut relations: Relations = "python3".parse().unwrap();
3971        relations.ensure_some_version("debhelper");
3972        assert_eq!(relations.to_string(), "python3, debhelper");
3973    }
3974
3975    #[test]
3976    fn test_ensure_some_version_exists_with_version() {
3977        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
3978        relations.ensure_some_version("debhelper");
3979        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3980    }
3981
3982    #[test]
3983    fn test_ensure_some_version_exists_no_version() {
3984        let mut relations: Relations = "debhelper".parse().unwrap();
3985        relations.ensure_some_version("debhelper");
3986        assert_eq!(relations.to_string(), "debhelper");
3987    }
3988
3989    #[test]
3990    fn test_ensure_substvar() {
3991        let mut relations: Relations = "python3".parse().unwrap();
3992        relations.ensure_substvar("${misc:Depends}").unwrap();
3993        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
3994    }
3995
3996    #[test]
3997    fn test_ensure_substvar_already_exists() {
3998        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
3999        relations.ensure_substvar("${misc:Depends}").unwrap();
4000        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
4001    }
4002
4003    #[test]
4004    fn test_ensure_substvar_empty_relations() {
4005        let mut relations: Relations = Relations::new();
4006        relations.ensure_substvar("${misc:Depends}").unwrap();
4007        assert_eq!(relations.to_string(), "${misc:Depends}");
4008    }
4009
4010    #[test]
4011    fn test_ensure_substvar_preserves_whitespace() {
4012        // Test with non-standard whitespace (multiple spaces)
4013        let (mut relations, _) = Relations::parse_relaxed("python3,  rustc", false);
4014        relations.ensure_substvar("${misc:Depends}").unwrap();
4015        // Should preserve the double-space pattern
4016        assert_eq!(relations.to_string(), "python3,  rustc,  ${misc:Depends}");
4017    }
4018
4019    #[test]
4020    fn test_ensure_substvar_to_existing_substvar() {
4021        // Test adding a substvar to existing substvar (no entries)
4022        // This reproduces the bug where space after comma is lost
4023        let (mut relations, _) = Relations::parse_relaxed("${shlibs:Depends}", true);
4024        relations.ensure_substvar("${misc:Depends}").unwrap();
4025        // Should have a space after the comma
4026        assert_eq!(relations.to_string(), "${shlibs:Depends}, ${misc:Depends}");
4027    }
4028
4029    #[test]
4030    fn test_drop_substvar_basic() {
4031        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
4032        relations.drop_substvar("${misc:Depends}");
4033        assert_eq!(relations.to_string(), "python3");
4034    }
4035
4036    #[test]
4037    fn test_drop_substvar_first_position() {
4038        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}, python3", true);
4039        relations.drop_substvar("${misc:Depends}");
4040        assert_eq!(relations.to_string(), "python3");
4041    }
4042
4043    #[test]
4044    fn test_drop_substvar_middle_position() {
4045        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
4046        relations.drop_substvar("${misc:Depends}");
4047        assert_eq!(relations.to_string(), "python3, rustc");
4048    }
4049
4050    #[test]
4051    fn test_drop_substvar_only_substvar() {
4052        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}", true);
4053        relations.drop_substvar("${misc:Depends}");
4054        assert_eq!(relations.to_string(), "");
4055    }
4056
4057    #[test]
4058    fn test_drop_substvar_not_exists() {
4059        let (mut relations, _) = Relations::parse_relaxed("python3, rustc", false);
4060        relations.drop_substvar("${misc:Depends}");
4061        assert_eq!(relations.to_string(), "python3, rustc");
4062    }
4063
4064    #[test]
4065    fn test_drop_substvar_multiple_substvars() {
4066        let (mut relations, _) =
4067            Relations::parse_relaxed("python3, ${misc:Depends}, ${shlibs:Depends}", true);
4068        relations.drop_substvar("${misc:Depends}");
4069        assert_eq!(relations.to_string(), "python3, ${shlibs:Depends}");
4070    }
4071
4072    #[test]
4073    fn test_drop_substvar_preserves_whitespace() {
4074        let (mut relations, _) = Relations::parse_relaxed("python3,  ${misc:Depends}", true);
4075        relations.drop_substvar("${misc:Depends}");
4076        assert_eq!(relations.to_string(), "python3");
4077    }
4078
4079    #[test]
4080    fn test_filter_entries_basic() {
4081        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4082        relations.filter_entries(|entry| entry.relations().any(|r| r.name().starts_with("python")));
4083        assert_eq!(relations.to_string(), "python3");
4084    }
4085
4086    #[test]
4087    fn test_filter_entries_keep_all() {
4088        let mut relations: Relations = "python3, debhelper".parse().unwrap();
4089        relations.filter_entries(|_| true);
4090        assert_eq!(relations.to_string(), "python3, debhelper");
4091    }
4092
4093    #[test]
4094    fn test_filter_entries_remove_all() {
4095        let mut relations: Relations = "python3, debhelper".parse().unwrap();
4096        relations.filter_entries(|_| false);
4097        assert_eq!(relations.to_string(), "");
4098    }
4099
4100    #[test]
4101    fn test_filter_entries_keep_middle() {
4102        let mut relations: Relations = "aaa, bbb, ccc".parse().unwrap();
4103        relations.filter_entries(|entry| entry.relations().any(|r| r.name() == "bbb"));
4104        assert_eq!(relations.to_string(), "bbb");
4105    }
4106
4107    // Tests for new convenience methods
4108
4109    #[test]
4110    fn test_is_sorted_wrap_and_sort_order() {
4111        // Sorted according to WrapAndSortOrder
4112        let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
4113        assert!(relations.is_sorted(&WrapAndSortOrder));
4114
4115        // Not sorted
4116        let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
4117        assert!(!relations.is_sorted(&WrapAndSortOrder));
4118
4119        // Build systems first (sorted alphabetically within their group)
4120        let (relations, _) =
4121            Relations::parse_relaxed("cdbs, debhelper-compat, python3, ${misc:Depends}", true);
4122        assert!(relations.is_sorted(&WrapAndSortOrder));
4123    }
4124
4125    #[test]
4126    fn test_is_sorted_default_order() {
4127        // Sorted alphabetically
4128        let relations: Relations = "aaa, bbb, ccc".parse().unwrap();
4129        assert!(relations.is_sorted(&DefaultSortingOrder));
4130
4131        // Not sorted
4132        let relations: Relations = "ccc, aaa, bbb".parse().unwrap();
4133        assert!(!relations.is_sorted(&DefaultSortingOrder));
4134
4135        // Special items at end
4136        let (relations, _) = Relations::parse_relaxed("aaa, bbb, ${misc:Depends}", true);
4137        assert!(relations.is_sorted(&DefaultSortingOrder));
4138    }
4139
4140    #[test]
4141    fn test_is_sorted_with_substvars() {
4142        // Substvars should be ignored by DefaultSortingOrder
4143        let (relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
4144        // This is considered sorted because ${misc:Depends} is ignored
4145        assert!(relations.is_sorted(&DefaultSortingOrder));
4146    }
4147
4148    #[test]
4149    fn test_drop_dependency_exists() {
4150        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4151        assert!(relations.drop_dependency("debhelper"));
4152        assert_eq!(relations.to_string(), "python3, rustc");
4153    }
4154
4155    #[test]
4156    fn test_drop_dependency_not_exists() {
4157        let mut relations: Relations = "python3, rustc".parse().unwrap();
4158        assert!(!relations.drop_dependency("nonexistent"));
4159        assert_eq!(relations.to_string(), "python3, rustc");
4160    }
4161
4162    #[test]
4163    fn test_drop_dependency_only_item() {
4164        let mut relations: Relations = "python3".parse().unwrap();
4165        assert!(relations.drop_dependency("python3"));
4166        assert_eq!(relations.to_string(), "");
4167    }
4168
4169    #[test]
4170    fn test_add_dependency_to_empty() {
4171        let mut relations: Relations = "".parse().unwrap();
4172        let entry = Entry::from(Relation::simple("python3"));
4173        relations.add_dependency(entry, None);
4174        assert_eq!(relations.to_string(), "python3");
4175    }
4176
4177    #[test]
4178    fn test_add_dependency_sorted_position() {
4179        let mut relations: Relations = "debhelper, rustc".parse().unwrap();
4180        let entry = Entry::from(Relation::simple("python3"));
4181        relations.add_dependency(entry, None);
4182        // Should be inserted in sorted position
4183        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4184    }
4185
4186    #[test]
4187    fn test_add_dependency_explicit_position() {
4188        let mut relations: Relations = "python3, rustc".parse().unwrap();
4189        let entry = Entry::from(Relation::simple("debhelper"));
4190        relations.add_dependency(entry, Some(0));
4191        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4192    }
4193
4194    #[test]
4195    fn test_add_dependency_build_system_first() {
4196        let mut relations: Relations = "python3, rustc".parse().unwrap();
4197        let entry = Entry::from(Relation::simple("debhelper-compat"));
4198        relations.add_dependency(entry, None);
4199        // debhelper-compat should be inserted first (build system)
4200        assert_eq!(relations.to_string(), "debhelper-compat, python3, rustc");
4201    }
4202
4203    #[test]
4204    fn test_add_dependency_at_end() {
4205        let mut relations: Relations = "debhelper, python3".parse().unwrap();
4206        let entry = Entry::from(Relation::simple("zzz-package"));
4207        relations.add_dependency(entry, None);
4208        // Should be added at the end (alphabetically after python3)
4209        assert_eq!(relations.to_string(), "debhelper, python3, zzz-package");
4210    }
4211
4212    #[test]
4213    fn test_add_dependency_to_single_entry() {
4214        // Regression test: ensure comma is added when inserting into single-entry Relations
4215        let mut relations: Relations = "python3-dulwich".parse().unwrap();
4216        let entry: Entry = "debhelper-compat (= 12)".parse().unwrap();
4217        relations.add_dependency(entry, None);
4218        // Should insert with comma separator
4219        assert_eq!(
4220            relations.to_string(),
4221            "debhelper-compat (= 12), python3-dulwich"
4222        );
4223    }
4224
4225    #[test]
4226    fn test_get_relation_exists() {
4227        let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
4228        let result = relations.get_relation("debhelper");
4229        assert!(result.is_ok());
4230        let (idx, entry) = result.unwrap();
4231        assert_eq!(idx, 1);
4232        assert_eq!(entry.to_string(), "debhelper (>= 12)");
4233    }
4234
4235    #[test]
4236    fn test_get_relation_not_exists() {
4237        let relations: Relations = "python3, rustc".parse().unwrap();
4238        let result = relations.get_relation("nonexistent");
4239        assert_eq!(result, Err("Package nonexistent not found".to_string()));
4240    }
4241
4242    #[test]
4243    fn test_get_relation_complex_rule() {
4244        let relations: Relations = "python3 | python3-minimal, rustc".parse().unwrap();
4245        let result = relations.get_relation("python3");
4246        assert_eq!(
4247            result,
4248            Err("Complex rule for python3, aborting".to_string())
4249        );
4250    }
4251
4252    #[test]
4253    fn test_iter_relations_for_simple() {
4254        let relations: Relations = "python3, debhelper, python3-dev".parse().unwrap();
4255        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4256        assert_eq!(entries.len(), 1);
4257        assert_eq!(entries[0].0, 0);
4258        assert_eq!(entries[0].1.to_string(), "python3");
4259    }
4260
4261    #[test]
4262    fn test_iter_relations_for_alternatives() {
4263        let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
4264        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4265        // Should find both the alternative entry and python3-dev is not included
4266        assert_eq!(entries.len(), 1);
4267        assert_eq!(entries[0].0, 0);
4268    }
4269
4270    #[test]
4271    fn test_iter_relations_for_not_found() {
4272        let relations: Relations = "python3, rustc".parse().unwrap();
4273        let entries: Vec<_> = relations.iter_relations_for("debhelper").collect();
4274        assert_eq!(entries.len(), 0);
4275    }
4276
4277    #[test]
4278    fn test_has_relation_exists() {
4279        let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4280        assert!(relations.has_relation("debhelper"));
4281        assert!(relations.has_relation("python3"));
4282        assert!(relations.has_relation("rustc"));
4283    }
4284
4285    #[test]
4286    fn test_has_relation_not_exists() {
4287        let relations: Relations = "python3, rustc".parse().unwrap();
4288        assert!(!relations.has_relation("debhelper"));
4289    }
4290
4291    #[test]
4292    fn test_has_relation_in_alternative() {
4293        let relations: Relations = "python3 | python3-minimal".parse().unwrap();
4294        assert!(relations.has_relation("python3"));
4295        assert!(relations.has_relation("python3-minimal"));
4296    }
4297
4298    #[test]
4299    fn test_sorting_order_wrap_and_sort_build_systems() {
4300        let order = WrapAndSortOrder;
4301        // Build systems should come before regular packages
4302        assert!(order.lt("debhelper", "python3"));
4303        assert!(order.lt("debhelper-compat", "rustc"));
4304        assert!(order.lt("cdbs", "aaa"));
4305        assert!(order.lt("dh-python", "python3"));
4306    }
4307
4308    #[test]
4309    fn test_sorting_order_wrap_and_sort_regular_packages() {
4310        let order = WrapAndSortOrder;
4311        // Regular packages sorted alphabetically
4312        assert!(order.lt("aaa", "bbb"));
4313        assert!(order.lt("python3", "rustc"));
4314        assert!(!order.lt("rustc", "python3"));
4315    }
4316
4317    #[test]
4318    fn test_sorting_order_wrap_and_sort_substvars() {
4319        let order = WrapAndSortOrder;
4320        // Substvars should come after regular packages
4321        assert!(order.lt("python3", "${misc:Depends}"));
4322        assert!(!order.lt("${misc:Depends}", "python3"));
4323        // But wrap-and-sort doesn't ignore them
4324        assert!(!order.ignore("${misc:Depends}"));
4325    }
4326
4327    #[test]
4328    fn test_sorting_order_default_special_items() {
4329        let order = DefaultSortingOrder;
4330        // Special items should come after regular items
4331        assert!(order.lt("python3", "${misc:Depends}"));
4332        assert!(order.lt("aaa", "@cdbs@"));
4333        // And should be ignored
4334        assert!(order.ignore("${misc:Depends}"));
4335        assert!(order.ignore("@cdbs@"));
4336        assert!(!order.ignore("python3"));
4337    }
4338
4339    #[test]
4340    fn test_is_special_package_name() {
4341        assert!(is_special_package_name("${misc:Depends}"));
4342        assert!(is_special_package_name("${shlibs:Depends}"));
4343        assert!(is_special_package_name("@cdbs@"));
4344        assert!(!is_special_package_name("python3"));
4345        assert!(!is_special_package_name("debhelper"));
4346    }
4347
4348    #[test]
4349    fn test_add_dependency_with_explicit_position() {
4350        // Test that add_dependency works with explicit position and preserves whitespace
4351        let mut relations: Relations = "python3,  rustc".parse().unwrap();
4352        let entry = Entry::from(Relation::simple("debhelper"));
4353        relations.add_dependency(entry, Some(1));
4354        // Should preserve the 2-space pattern from the original
4355        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc");
4356    }
4357
4358    #[test]
4359    fn test_whitespace_detection_single_space() {
4360        let mut relations: Relations = "python3, rustc".parse().unwrap();
4361        let entry = Entry::from(Relation::simple("debhelper"));
4362        relations.add_dependency(entry, Some(1));
4363        assert_eq!(relations.to_string(), "python3, debhelper, rustc");
4364    }
4365
4366    #[test]
4367    fn test_whitespace_detection_multiple_spaces() {
4368        let mut relations: Relations = "python3,  rustc,  gcc".parse().unwrap();
4369        let entry = Entry::from(Relation::simple("debhelper"));
4370        relations.add_dependency(entry, Some(1));
4371        // Should detect and use the 2-space pattern
4372        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc,  gcc");
4373    }
4374
4375    #[test]
4376    fn test_whitespace_detection_mixed_patterns() {
4377        // When patterns differ, use the most common one
4378        let mut relations: Relations = "a, b, c,  d, e".parse().unwrap();
4379        let entry = Entry::from(Relation::simple("x"));
4380        relations.push(entry);
4381        // Three single-space (after a, b, d), one double-space (after c)
4382        // Should use single space as it's most common
4383        assert_eq!(relations.to_string(), "a, b, c,  d, e, x");
4384    }
4385
4386    #[test]
4387    fn test_whitespace_detection_newlines() {
4388        let mut relations: Relations = "python3,\n rustc".parse().unwrap();
4389        let entry = Entry::from(Relation::simple("debhelper"));
4390        relations.add_dependency(entry, Some(1));
4391        // Detects full pattern including newline
4392        assert_eq!(relations.to_string(), "python3,\n debhelper,\n rustc");
4393    }
4394
4395    #[test]
4396    fn test_append_with_newline_no_trailing() {
4397        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4398        let entry = Entry::from(Relation::simple("blah"));
4399        relations.add_dependency(entry, None);
4400        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4401    }
4402
4403    #[test]
4404    fn test_append_with_trailing_newline() {
4405        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4406        let entry = Entry::from(Relation::simple("blah"));
4407        relations.add_dependency(entry, None);
4408        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4409    }
4410
4411    #[test]
4412    fn test_append_with_4_space_indent() {
4413        let mut relations: Relations = "foo,\n    bar".parse().unwrap();
4414        let entry = Entry::from(Relation::simple("blah"));
4415        relations.add_dependency(entry, None);
4416        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4417    }
4418
4419    #[test]
4420    fn test_append_with_4_space_and_trailing_newline() {
4421        let mut relations: Relations = "foo,\n    bar\n".parse().unwrap();
4422        let entry = Entry::from(Relation::simple("blah"));
4423        relations.add_dependency(entry, None);
4424        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4425    }
4426
4427    #[test]
4428    fn test_odd_syntax_append_no_trailing() {
4429        let mut relations: Relations = "\n foo\n , bar".parse().unwrap();
4430        let entry = Entry::from(Relation::simple("blah"));
4431        relations.add_dependency(entry, None);
4432        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4433    }
4434
4435    #[test]
4436    fn test_odd_syntax_append_with_trailing() {
4437        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4438        let entry = Entry::from(Relation::simple("blah"));
4439        relations.add_dependency(entry, None);
4440        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4441    }
4442
4443    #[test]
4444    fn test_insert_at_1_no_trailing() {
4445        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4446        let entry = Entry::from(Relation::simple("blah"));
4447        relations.add_dependency(entry, Some(1));
4448        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4449    }
4450
4451    #[test]
4452    fn test_insert_at_1_with_trailing() {
4453        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4454        let entry = Entry::from(Relation::simple("blah"));
4455        relations.add_dependency(entry, Some(1));
4456        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4457    }
4458
4459    #[test]
4460    fn test_odd_syntax_insert_at_1() {
4461        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4462        let entry = Entry::from(Relation::simple("blah"));
4463        relations.add_dependency(entry, Some(1));
4464        assert_eq!(relations.to_string(), "\n foo\n , blah\n , bar");
4465    }
4466
4467    #[test]
4468    fn test_relations_preserves_exact_whitespace() {
4469        // Test that Relations preserves exact whitespace from input
4470        let input =
4471            "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], pkg-config";
4472
4473        let relations: Relations = input.parse().unwrap();
4474
4475        // The whitespace should be preserved in the syntax tree
4476        assert_eq!(
4477            relations.to_string(),
4478            input,
4479            "Relations should preserve exact whitespace from input"
4480        );
4481    }
4482
4483    #[test]
4484    fn test_remove_entry_preserves_indentation() {
4485        // Test that removing an entry preserves the indentation pattern
4486        let input = "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config";
4487
4488        let mut relations: Relations = input.parse().unwrap();
4489
4490        // Find and remove dh-systemd entry (index 2)
4491        let mut to_remove = Vec::new();
4492        for (idx, entry) in relations.entries().enumerate() {
4493            for relation in entry.relations() {
4494                if relation.name() == "dh-systemd" {
4495                    to_remove.push(idx);
4496                    break;
4497                }
4498            }
4499        }
4500
4501        for idx in to_remove.into_iter().rev() {
4502            relations.remove_entry(idx);
4503        }
4504
4505        let output = relations.to_string();
4506        println!("After removal: '{}'", output);
4507
4508        // The 4-space indentation should be preserved
4509        assert!(
4510            output.contains("\n    libsystemd-dev"),
4511            "Expected 4-space indentation to be preserved, but got:\n'{}'",
4512            output
4513        );
4514    }
4515
4516    #[test]
4517    fn test_relation_is_implied_by_same_package() {
4518        // Same package name with compatible version constraints
4519        let inner = Relation::new(
4520            "pkg",
4521            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4522        );
4523        let outer = Relation::new(
4524            "pkg",
4525            Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())),
4526        );
4527        assert!(inner.is_implied_by(&outer));
4528    }
4529
4530    #[test]
4531    fn test_relation_is_implied_by_different_package() {
4532        // Different package names should not imply
4533        let inner = Relation::new("pkg1", None);
4534        let outer = Relation::new("pkg2", None);
4535        assert!(!inner.is_implied_by(&outer));
4536    }
4537
4538    #[test]
4539    fn test_relation_is_implied_by_no_version() {
4540        // No version constraint is implied by any version
4541        let inner = Relation::new("pkg", None);
4542        let outer = Relation::new(
4543            "pkg",
4544            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4545        );
4546        assert!(inner.is_implied_by(&outer));
4547    }
4548
4549    #[test]
4550    fn test_relation_is_implied_by_identical() {
4551        // Identical relations imply each other
4552        let inner = Relation::new(
4553            "pkg",
4554            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4555        );
4556        let outer = Relation::new(
4557            "pkg",
4558            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4559        );
4560        assert!(inner.is_implied_by(&outer));
4561        assert!(outer.is_implied_by(&inner));
4562    }
4563
4564    #[test]
4565    fn test_relation_is_implied_by_greater_than_equal() {
4566        // pkg >= 1.0 is implied by pkg >= 2.0
4567        let inner = Relation::new(
4568            "pkg",
4569            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4570        );
4571        let outer = Relation::new(
4572            "pkg",
4573            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
4574        );
4575        assert!(inner.is_implied_by(&outer));
4576        assert!(!outer.is_implied_by(&inner));
4577
4578        // pkg >= 1.0 is implied by pkg = 2.0
4579        let outer = Relation::new(
4580            "pkg",
4581            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4582        );
4583        assert!(inner.is_implied_by(&outer));
4584
4585        // pkg >= 1.0 is implied by pkg >> 1.5
4586        let outer = Relation::new(
4587            "pkg",
4588            Some((VersionConstraint::GreaterThan, "1.5".parse().unwrap())),
4589        );
4590        assert!(inner.is_implied_by(&outer));
4591
4592        // pkg >= 3.0 is NOT implied by pkg >> 3.0 (>> 3.0 doesn't include 3.0 itself)
4593        let inner = Relation::new(
4594            "pkg",
4595            Some((VersionConstraint::GreaterThanEqual, "3.0".parse().unwrap())),
4596        );
4597        let outer = Relation::new(
4598            "pkg",
4599            Some((VersionConstraint::GreaterThan, "3.0".parse().unwrap())),
4600        );
4601        assert!(!inner.is_implied_by(&outer));
4602    }
4603
4604    #[test]
4605    fn test_relation_is_implied_by_less_than_equal() {
4606        // pkg <= 2.0 is implied by pkg <= 1.0
4607        let inner = Relation::new(
4608            "pkg",
4609            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4610        );
4611        let outer = Relation::new(
4612            "pkg",
4613            Some((VersionConstraint::LessThanEqual, "1.0".parse().unwrap())),
4614        );
4615        assert!(inner.is_implied_by(&outer));
4616        assert!(!outer.is_implied_by(&inner));
4617
4618        // pkg <= 2.0 is implied by pkg = 1.0
4619        let outer = Relation::new(
4620            "pkg",
4621            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4622        );
4623        assert!(inner.is_implied_by(&outer));
4624
4625        // pkg <= 2.0 is implied by pkg << 1.5
4626        let outer = Relation::new(
4627            "pkg",
4628            Some((VersionConstraint::LessThan, "1.5".parse().unwrap())),
4629        );
4630        assert!(inner.is_implied_by(&outer));
4631    }
4632
4633    #[test]
4634    fn test_relation_is_implied_by_equal() {
4635        // pkg = 1.0 is only implied by pkg = 1.0
4636        let inner = Relation::new(
4637            "pkg",
4638            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4639        );
4640        let outer = Relation::new(
4641            "pkg",
4642            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4643        );
4644        assert!(inner.is_implied_by(&outer));
4645
4646        // Not implied by different version
4647        let outer = Relation::new(
4648            "pkg",
4649            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4650        );
4651        assert!(!inner.is_implied_by(&outer));
4652
4653        // Not implied by >= constraint
4654        let outer = Relation::new(
4655            "pkg",
4656            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4657        );
4658        assert!(!inner.is_implied_by(&outer));
4659    }
4660
4661    #[test]
4662    fn test_relation_is_implied_by_greater_than() {
4663        // pkg >> 1.0 is implied by pkg >> 2.0
4664        let inner = Relation::new(
4665            "pkg",
4666            Some((VersionConstraint::GreaterThan, "1.0".parse().unwrap())),
4667        );
4668        let outer = Relation::new(
4669            "pkg",
4670            Some((VersionConstraint::GreaterThan, "2.0".parse().unwrap())),
4671        );
4672        assert!(inner.is_implied_by(&outer));
4673
4674        // pkg >> 1.0 is implied by pkg = 2.0
4675        let outer = Relation::new(
4676            "pkg",
4677            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4678        );
4679        assert!(inner.is_implied_by(&outer));
4680
4681        // pkg >> 1.0 is implied by pkg >= 1.5 (strictly greater)
4682        let outer = Relation::new(
4683            "pkg",
4684            Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())),
4685        );
4686        assert!(inner.is_implied_by(&outer));
4687
4688        // pkg >> 1.0 is NOT implied by pkg >= 1.0 (could be equal)
4689        let outer = Relation::new(
4690            "pkg",
4691            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4692        );
4693        assert!(!inner.is_implied_by(&outer));
4694    }
4695
4696    #[test]
4697    fn test_relation_is_implied_by_less_than() {
4698        // pkg << 2.0 is implied by pkg << 1.0
4699        let inner = Relation::new(
4700            "pkg",
4701            Some((VersionConstraint::LessThan, "2.0".parse().unwrap())),
4702        );
4703        let outer = Relation::new(
4704            "pkg",
4705            Some((VersionConstraint::LessThan, "1.0".parse().unwrap())),
4706        );
4707        assert!(inner.is_implied_by(&outer));
4708
4709        // pkg << 2.0 is implied by pkg = 1.0
4710        let outer = Relation::new(
4711            "pkg",
4712            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4713        );
4714        assert!(inner.is_implied_by(&outer));
4715
4716        // pkg << 2.0 is implied by pkg <= 1.5 (strictly less)
4717        let outer = Relation::new(
4718            "pkg",
4719            Some((VersionConstraint::LessThanEqual, "1.5".parse().unwrap())),
4720        );
4721        assert!(inner.is_implied_by(&outer));
4722    }
4723
4724    #[test]
4725    fn test_relation_is_implied_by_incompatible_constraints() {
4726        // >= and <= are incompatible
4727        let inner = Relation::new(
4728            "pkg",
4729            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4730        );
4731        let outer = Relation::new(
4732            "pkg",
4733            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4734        );
4735        assert!(!inner.is_implied_by(&outer));
4736        assert!(!outer.is_implied_by(&inner));
4737    }
4738
4739    #[test]
4740    fn test_entry_is_implied_by_identical() {
4741        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4742        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4743        assert!(inner.is_implied_by(&outer));
4744    }
4745
4746    #[test]
4747    fn test_entry_is_implied_by_or_group() {
4748        // "pkg >= 1.0" is implied by "pkg >= 1.5 | libc6"
4749        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4750        let outer: Entry = "pkg (>= 1.5) | libc6".parse().unwrap();
4751        assert!(inner.is_implied_by(&outer));
4752    }
4753
4754    #[test]
4755    fn test_entry_is_implied_by_simple_or() {
4756        // "pkg1 | pkg2" is implied by "pkg1" (first alternative satisfies)
4757        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4758        let outer: Entry = "pkg1".parse().unwrap();
4759        assert!(inner.is_implied_by(&outer));
4760
4761        // Also implied by "pkg2"
4762        let outer: Entry = "pkg2".parse().unwrap();
4763        assert!(inner.is_implied_by(&outer));
4764    }
4765
4766    #[test]
4767    fn test_entry_is_implied_by_not_implied() {
4768        // "pkg >= 2.0" is NOT implied by "pkg >= 1.0"
4769        let inner: Entry = "pkg (>= 2.0)".parse().unwrap();
4770        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4771        assert!(!inner.is_implied_by(&outer));
4772    }
4773
4774    #[test]
4775    fn test_entry_is_implied_by_different_packages() {
4776        let inner: Entry = "pkg1".parse().unwrap();
4777        let outer: Entry = "pkg2".parse().unwrap();
4778        assert!(!inner.is_implied_by(&outer));
4779    }
4780
4781    #[test]
4782    fn test_entry_is_implied_by_complex_or() {
4783        // "pkg1 | pkg2" is implied by "pkg1 | pkg2" (identical)
4784        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4785        let outer: Entry = "pkg1 | pkg2".parse().unwrap();
4786        assert!(inner.is_implied_by(&outer));
4787
4788        // "pkg1 | pkg2" is implied by "pkg1 | pkg2 | pkg3" (one matches)
4789        let outer: Entry = "pkg1 | pkg2 | pkg3".parse().unwrap();
4790        assert!(inner.is_implied_by(&outer));
4791    }
4792
4793    #[test]
4794    fn test_parse_version_with_epoch() {
4795        // Test parsing version strings with epoch (e.g., "1:2.3.2-2~")
4796        // The colon should be treated as part of the version, not as a delimiter
4797        let input = "amule-dbg (<< 1:2.3.2-2~)";
4798        let parsed: Relations = input.parse().unwrap();
4799        assert_eq!(parsed.to_string(), input);
4800        assert_eq!(parsed.entries().count(), 1);
4801        let entry = parsed.entries().next().unwrap();
4802        assert_eq!(entry.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4803        assert_eq!(entry.relations().count(), 1);
4804        let relation = entry.relations().next().unwrap();
4805        assert_eq!(relation.name(), "amule-dbg");
4806        assert_eq!(relation.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4807        assert_eq!(
4808            relation.version(),
4809            Some((VersionConstraint::LessThan, "1:2.3.2-2~".parse().unwrap()))
4810        );
4811    }
4812
4813    #[test]
4814    fn test_ensure_relation_add_new() {
4815        // Test adding a new relation that doesn't exist yet
4816        let mut relations: Relations = "python3".parse().unwrap();
4817        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4818        let added = relations.ensure_relation(new_entry);
4819        assert!(added);
4820        // debhelper is inserted in sorted position (alphabetically before python3)
4821        assert_eq!(relations.to_string(), "debhelper (>= 12), python3");
4822    }
4823
4824    #[test]
4825    fn test_ensure_relation_already_satisfied() {
4826        // Test that a relation is not added if it's already satisfied by a stronger constraint
4827        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
4828        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4829        let added = relations.ensure_relation(new_entry);
4830        assert!(!added);
4831        assert_eq!(relations.to_string(), "debhelper (>= 13)");
4832    }
4833
4834    #[test]
4835    fn test_ensure_relation_replace_weaker() {
4836        // Test that a weaker relation is replaced with a stronger one
4837        let mut relations: Relations = "debhelper (>= 11)".parse().unwrap();
4838        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4839        let added = relations.ensure_relation(new_entry);
4840        assert!(added);
4841        assert_eq!(relations.to_string(), "debhelper (>= 13)");
4842    }
4843
4844    #[test]
4845    fn test_ensure_relation_replace_multiple_weaker() {
4846        // Test that multiple weaker relations are replaced/removed when a stronger one is added
4847        let mut relations: Relations = "debhelper (>= 11), debhelper (>= 10), python3"
4848            .parse()
4849            .unwrap();
4850        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4851        let added = relations.ensure_relation(new_entry);
4852        assert!(added);
4853        assert_eq!(relations.to_string(), "debhelper (>= 13), python3");
4854    }
4855
4856    #[test]
4857    fn test_ensure_relation_identical_entry() {
4858        // Test that an identical entry is not added again
4859        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
4860        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4861        let added = relations.ensure_relation(new_entry);
4862        assert!(!added);
4863        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4864    }
4865
4866    #[test]
4867    fn test_ensure_relation_no_version_constraint() {
4868        // Test that a relation without version constraint is added
4869        let mut relations: Relations = "python3".parse().unwrap();
4870        let new_entry: Entry = "debhelper".parse().unwrap();
4871        let added = relations.ensure_relation(new_entry);
4872        assert!(added);
4873        // debhelper is inserted in sorted position (alphabetically before python3)
4874        assert_eq!(relations.to_string(), "debhelper, python3");
4875    }
4876
4877    #[test]
4878    fn test_ensure_relation_strengthen_unversioned() {
4879        // Test that a versioned constraint replaces an unversioned one
4880        // An unversioned dependency is weaker than a versioned one
4881        let mut relations: Relations = "debhelper".parse().unwrap();
4882        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4883        let added = relations.ensure_relation(new_entry);
4884        assert!(added);
4885        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4886    }
4887
4888    #[test]
4889    fn test_ensure_relation_versioned_implies_unversioned() {
4890        // Test that an unversioned dependency is already satisfied by a versioned one
4891        // A versioned dependency is stronger and implies the unversioned one
4892        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
4893        let new_entry: Entry = "debhelper".parse().unwrap();
4894        let added = relations.ensure_relation(new_entry);
4895        assert!(!added);
4896        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4897    }
4898
4899    #[test]
4900    fn test_ensure_relation_preserves_whitespace() {
4901        // Test that whitespace is preserved when adding a new relation
4902        let mut relations: Relations = "python3,  rustc".parse().unwrap();
4903        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4904        let added = relations.ensure_relation(new_entry);
4905        assert!(added);
4906        // debhelper is inserted in sorted position (alphabetically before python3 and rustc)
4907        assert_eq!(relations.to_string(), "debhelper (>= 12),  python3,  rustc");
4908    }
4909
4910    #[test]
4911    fn test_ensure_relation_empty_relations() {
4912        // Test adding to empty relations
4913        let mut relations: Relations = Relations::new();
4914        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4915        let added = relations.ensure_relation(new_entry);
4916        assert!(added);
4917        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4918    }
4919
4920    #[test]
4921    fn test_ensure_relation_alternative_dependencies() {
4922        // Test with alternative dependencies (|)
4923        let mut relations: Relations = "python3 | python3-minimal".parse().unwrap();
4924        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4925        let added = relations.ensure_relation(new_entry);
4926        assert!(added);
4927        // debhelper is inserted in sorted position (alphabetically before python3)
4928        assert_eq!(
4929            relations.to_string(),
4930            "debhelper (>= 12), python3 | python3-minimal"
4931        );
4932    }
4933
4934    #[test]
4935    fn test_ensure_relation_replace_in_middle() {
4936        // Test that replacing a weaker entry in the middle preserves order
4937        let mut relations: Relations = "python3, debhelper (>= 11), rustc".parse().unwrap();
4938        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4939        let added = relations.ensure_relation(new_entry);
4940        assert!(added);
4941        assert_eq!(relations.to_string(), "python3, debhelper (>= 13), rustc");
4942    }
4943
4944    #[test]
4945    fn test_ensure_relation_with_different_package() {
4946        // Test that adding a different package doesn't affect existing ones
4947        let mut relations: Relations = "python3, debhelper (>= 12)".parse().unwrap();
4948        let new_entry: Entry = "rustc".parse().unwrap();
4949        let added = relations.ensure_relation(new_entry);
4950        assert!(added);
4951        assert_eq!(relations.to_string(), "python3, debhelper (>= 12), rustc");
4952    }
4953
4954    #[test]
4955    fn test_parse_invalid_token_in_arch_list() {
4956        let input = "foo [>= bar]";
4957        let result: Result<Relations, _> = input.parse();
4958        assert!(
4959            result.is_err(),
4960            "Expected error for invalid token in architecture list"
4961        );
4962    }
4963
4964    #[test]
4965    fn test_parse_invalid_token_in_profile_list() {
4966        let input = "foo <[] baz>";
4967        let result: Result<Relations, _> = input.parse();
4968        assert!(
4969            result.is_err(),
4970            "Expected error for invalid token in profile list"
4971        );
4972    }
4973
4974    #[test]
4975    fn test_parse_relaxed_unterminated_arch_list() {
4976        let (relations, errors) = Relations::parse_relaxed("libc6 [", true);
4977        assert!(!errors.is_empty());
4978        assert_eq!(relations.to_string(), "libc6 [");
4979    }
4980
4981    #[test]
4982    fn test_parse_relaxed_partial_arch_name() {
4983        let (relations, errors) = Relations::parse_relaxed("libc6 [amd", true);
4984        assert!(!errors.is_empty());
4985        assert_eq!(relations.to_string(), "libc6 [amd");
4986    }
4987
4988    #[test]
4989    fn test_parse_relaxed_unterminated_profile_list() {
4990        let (relations, errors) = Relations::parse_relaxed("libc6 <cross", true);
4991        assert!(!errors.is_empty());
4992        assert_eq!(relations.to_string(), "libc6 <cross");
4993    }
4994
4995    #[test]
4996    fn test_parse_relaxed_unterminated_substvar() {
4997        let (relations, errors) = Relations::parse_relaxed("${shlibs:Depends", true);
4998        assert!(!errors.is_empty());
4999        assert_eq!(relations.to_string(), "${shlibs:Depends");
5000    }
5001
5002    #[test]
5003    fn test_parse_relaxed_empty_substvar() {
5004        let (relations, errors) = Relations::parse_relaxed("${", true);
5005        assert!(!errors.is_empty());
5006        assert_eq!(relations.to_string(), "${");
5007    }
5008
5009    #[test]
5010    fn test_parse_with_comments() {
5011        let input = "dh-python,\nlibsvn-dev,\n#               python-all-dbg (>= 2.6.6-3),\npython3-all-dev,\n#               python3-all-dbg,\npython3-docutils";
5012        let relations: Relations = input.parse().unwrap();
5013        let entries: Vec<_> = relations.entries().collect();
5014        assert_eq!(entries.len(), 4);
5015        assert_eq!(entries[0].to_string(), "dh-python");
5016        assert_eq!(entries[1].to_string(), "libsvn-dev");
5017        assert_eq!(entries[2].to_string(), "python3-all-dev");
5018        assert_eq!(entries[3].to_string(), "python3-docutils");
5019        // Round-trip preserves comments
5020        assert_eq!(relations.to_string(), input);
5021    }
5022
5023    #[test]
5024    fn test_remove_entry_with_adjacent_comment() {
5025        let input = "dh-python,\n#  commented-out,\npython3-all-dev";
5026        let mut relations: Relations = input.parse().unwrap();
5027        assert_eq!(relations.entries().count(), 2);
5028        relations.remove_entry(0);
5029        assert_eq!(relations.entries().count(), 1);
5030        assert_eq!(
5031            relations.entries().next().unwrap().to_string(),
5032            "python3-all-dev"
5033        );
5034    }
5035
5036    #[test]
5037    fn test_insert_entry_with_comments_present() {
5038        let input = "dh-python,\n#  commented-out,\npython3-all-dev";
5039        let mut relations: Relations = input.parse().unwrap();
5040        let new_entry: Entry = "libfoo-dev".parse().unwrap();
5041        relations.push(new_entry);
5042        // New entry should be appended
5043        let entries: Vec<_> = relations.entries().collect();
5044        assert_eq!(entries.len(), 3);
5045        assert_eq!(entries[2].to_string(), "libfoo-dev");
5046    }
5047
5048    #[test]
5049    fn test_drop_dependency_with_comments() {
5050        let input = "dh-python,\n#  commented-out,\npython3-all-dev,\nlibfoo-dev";
5051        let mut relations: Relations = input.parse().unwrap();
5052        assert!(relations.drop_dependency("python3-all-dev"));
5053        let entries: Vec<_> = relations.entries().collect();
5054        assert_eq!(entries.len(), 2);
5055        assert_eq!(entries[0].to_string(), "dh-python");
5056        assert_eq!(entries[1].to_string(), "libfoo-dev");
5057    }
5058
5059    #[test]
5060    fn test_relation_remove_first_with_malformed_tree() {
5061        // Relaxed parsing can produce ERROR nodes inside an entry.
5062        // Removing the first relation should not panic on unexpected tokens.
5063        let (relations, errors) = Relations::parse_relaxed("foo @ bar | baz", false);
5064        assert!(!errors.is_empty());
5065        let entry = relations.get_entry(0).unwrap();
5066        let mut relation = entry.get_relation(0).unwrap();
5067        // Should not panic
5068        relation.remove();
5069    }
5070}