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