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.name_token().map(|token| token.text().to_string())
2396    }
2397
2398    /// Return the text range of the package-name token, if present.
2399    ///
2400    /// Useful for editor tooling that wants to highlight or attach metadata
2401    /// to the package name itself, separate from any version constraint or
2402    /// architecture qualifier.
2403    ///
2404    /// # Example
2405    /// ```
2406    /// use debian_control::lossless::relations::Relation;
2407    /// let relation: Relation = "samba (>= 4.0)".parse().unwrap();
2408    /// let range = relation.name_range().unwrap();
2409    /// assert_eq!(&relation.to_string()[range], "samba");
2410    /// ```
2411    pub fn name_range(&self) -> Option<rowan::TextRange> {
2412        self.name_token().map(|token| token.text_range())
2413    }
2414
2415    fn name_token(&self) -> Option<crate::lossless::relations::SyntaxToken> {
2416        self.0.children_with_tokens().find_map(|it| match it {
2417            SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2418            _ => None,
2419        })
2420    }
2421
2422    /// Return the name of the package in the relation.
2423    ///
2424    /// # Panics
2425    /// Panics if the relation has no package name (e.g. malformed input).
2426    /// Prefer [`try_name`](Self::try_name) for potentially malformed relations.
2427    ///
2428    /// # Example
2429    /// ```
2430    /// use debian_control::lossless::relations::Relation;
2431    /// let relation = Relation::simple("samba");
2432    /// assert_eq!(relation.name(), "samba");
2433    /// ```
2434    #[deprecated(
2435        since = "0.3.6",
2436        note = "Use try_name() instead, which returns Option<String>"
2437    )]
2438    pub fn name(&self) -> String {
2439        self.try_name().expect("Relation has no package name")
2440    }
2441
2442    /// Return the archqual
2443    ///
2444    /// # Example
2445    /// ```
2446    /// use debian_control::lossless::relations::Relation;
2447    /// let relation: Relation = "samba:any".parse().unwrap();
2448    /// assert_eq!(relation.archqual(), Some("any".to_string()));
2449    /// ```
2450    pub fn archqual(&self) -> Option<String> {
2451        let archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2452        let node = if let Some(archqual) = archqual {
2453            archqual.children_with_tokens().find_map(|it| match it {
2454                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2455                _ => None,
2456            })
2457        } else {
2458            None
2459        };
2460        node.map(|n| n.text().to_string())
2461    }
2462
2463    /// Set the architecture qualifier for this relation.
2464    ///
2465    /// # Example
2466    /// ```
2467    /// use debian_control::lossless::relations::Relation;
2468    /// let mut relation = Relation::simple("samba");
2469    /// relation.set_archqual("any");
2470    /// assert_eq!(relation.to_string(), "samba:any");
2471    /// ```
2472    pub fn set_archqual(&mut self, archqual: &str) {
2473        let mut builder = GreenNodeBuilder::new();
2474        builder.start_node(ARCHQUAL.into());
2475        builder.token(COLON.into(), ":");
2476        builder.token(IDENT.into(), archqual);
2477        builder.finish_node();
2478
2479        let node_archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2480        if let Some(node_archqual) = node_archqual {
2481            self.0.splice_children(
2482                node_archqual.index()..node_archqual.index() + 1,
2483                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2484            );
2485        } else {
2486            let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2487            let idx = if let Some(name_node) = name_node {
2488                name_node.index() + 1
2489            } else {
2490                0
2491            };
2492            self.0.splice_children(
2493                idx..idx,
2494                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2495            );
2496        }
2497    }
2498
2499    /// Return the version constraint and the version it is constrained to.
2500    pub fn version(&self) -> Option<(VersionConstraint, Version)> {
2501        let vc = self.0.children().find(|n| n.kind() == VERSION);
2502        let vc = vc.as_ref()?;
2503        let constraint = vc.children().find(|n| n.kind() == CONSTRAINT);
2504
2505        // Collect all IDENT and COLON tokens to handle versions with epochs (e.g., "1:2.3.2-2~")
2506        let version_str: String = vc
2507            .children_with_tokens()
2508            .filter_map(|it| match it {
2509                SyntaxElement::Token(token) if token.kind() == IDENT || token.kind() == COLON => {
2510                    Some(token.text().to_string())
2511                }
2512                _ => None,
2513            })
2514            .collect();
2515
2516        if let Some(constraint) = constraint {
2517            if !version_str.is_empty() {
2518                let vc: VersionConstraint = constraint.to_string().parse().unwrap();
2519                Some((vc, version_str.parse().unwrap()))
2520            } else {
2521                None
2522            }
2523        } else {
2524            None
2525        }
2526    }
2527
2528    /// Set the version constraint for this relation
2529    ///
2530    /// # Example
2531    /// ```
2532    /// use debian_control::lossless::relations::{Relation};
2533    /// use debian_control::relations::VersionConstraint;
2534    /// let mut relation = Relation::simple("samba");
2535    /// relation.set_version(Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2536    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2537    /// ```
2538    pub fn set_version(&mut self, version_constraint: Option<(VersionConstraint, Version)>) {
2539        let current_version = self.0.children().find(|n| n.kind() == VERSION);
2540        if let Some((vc, version)) = version_constraint {
2541            let mut builder = GreenNodeBuilder::new();
2542            builder.start_node(VERSION.into());
2543            builder.token(L_PARENS.into(), "(");
2544            builder.start_node(CONSTRAINT.into());
2545            match vc {
2546                VersionConstraint::GreaterThanEqual => {
2547                    builder.token(R_ANGLE.into(), ">");
2548                    builder.token(EQUAL.into(), "=");
2549                }
2550                VersionConstraint::LessThanEqual => {
2551                    builder.token(L_ANGLE.into(), "<");
2552                    builder.token(EQUAL.into(), "=");
2553                }
2554                VersionConstraint::Equal => {
2555                    builder.token(EQUAL.into(), "=");
2556                }
2557                VersionConstraint::GreaterThan => {
2558                    builder.token(R_ANGLE.into(), ">");
2559                }
2560                VersionConstraint::LessThan => {
2561                    builder.token(L_ANGLE.into(), "<");
2562                }
2563            }
2564            builder.finish_node(); // CONSTRAINT
2565            builder.token(WHITESPACE.into(), " ");
2566            tokenize_version(&mut builder, &version);
2567            builder.token(R_PARENS.into(), ")");
2568            builder.finish_node(); // VERSION
2569
2570            if let Some(current_version) = current_version {
2571                self.0.splice_children(
2572                    current_version.index()..current_version.index() + 1,
2573                    vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2574                );
2575            } else {
2576                let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2577                let idx = if let Some(name_node) = name_node {
2578                    name_node.index() + 1
2579                } else {
2580                    0
2581                };
2582                let new_children = vec![
2583                    GreenToken::new(WHITESPACE.into(), " ").into(),
2584                    builder.finish().into(),
2585                ];
2586                let new_root = SyntaxNode::new_root_mut(
2587                    self.0.green().splice_children(idx..idx, new_children),
2588                );
2589                if let Some(parent) = self.0.parent() {
2590                    parent
2591                        .splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2592                    self.0 = parent
2593                        .children_with_tokens()
2594                        .nth(self.0.index())
2595                        .unwrap()
2596                        .clone()
2597                        .into_node()
2598                        .unwrap();
2599                } else {
2600                    self.0 = new_root;
2601                }
2602            }
2603        } else if let Some(current_version) = current_version {
2604            // Remove any whitespace before the version token
2605            while let Some(prev) = current_version.prev_sibling_or_token() {
2606                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2607                    prev.detach();
2608                } else {
2609                    break;
2610                }
2611            }
2612            current_version.detach();
2613        }
2614    }
2615
2616    /// Return an iterator over the architectures for this relation
2617    ///
2618    /// # Example
2619    /// ```
2620    /// use debian_control::lossless::relations::Relation;
2621    /// let relation: Relation = "samba [amd64]".parse().unwrap();
2622    /// assert_eq!(relation.architectures().unwrap().collect::<Vec<_>>(), vec!["amd64".to_string()]);
2623    /// ```
2624    pub fn architectures(&self) -> Option<impl Iterator<Item = String> + '_> {
2625        let architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES)?;
2626
2627        Some(architectures.children_with_tokens().filter_map(|node| {
2628            let token = node.as_token()?;
2629            if token.kind() == IDENT {
2630                Some(token.text().to_string())
2631            } else {
2632                None
2633            }
2634        }))
2635    }
2636
2637    /// Returns an iterator over the build profiles for this relation
2638    ///
2639    /// # Example
2640    /// ```
2641    /// use debian_control::lossless::relations::{Relation};
2642    /// use debian_control::relations::{BuildProfile};
2643    /// let relation: Relation = "samba <!nocheck>".parse().unwrap();
2644    /// assert_eq!(relation.profiles().collect::<Vec<_>>(), vec![vec![BuildProfile::Disabled("nocheck".to_string())]]);
2645    /// ```
2646    pub fn profiles(&self) -> impl Iterator<Item = Vec<BuildProfile>> + '_ {
2647        let profiles = self.0.children().filter(|n| n.kind() == PROFILES);
2648
2649        profiles.map(|profile| {
2650            // iterate over nodes separated by whitespace tokens
2651            let mut ret = vec![];
2652            let mut current = vec![];
2653            for token in profile.children_with_tokens() {
2654                match token.kind() {
2655                    WHITESPACE | NEWLINE => {
2656                        if !current.is_empty() {
2657                            ret.push(current.join("").parse::<BuildProfile>().unwrap());
2658                            current = vec![];
2659                        }
2660                    }
2661                    L_ANGLE | R_ANGLE => {}
2662                    _ => {
2663                        current.push(token.to_string());
2664                    }
2665                }
2666            }
2667            if !current.is_empty() {
2668                ret.push(current.concat().parse().unwrap());
2669            }
2670            ret
2671        })
2672    }
2673
2674    /// Return an iterator over the text ranges of each build-profile name
2675    /// token (the IDENT inside `<...>` groups).
2676    ///
2677    /// Useful for tooling that wants to highlight, hover over, or navigate
2678    /// from a specific profile name. The leading `!` of disabled profiles is
2679    /// not included.
2680    ///
2681    /// # Example
2682    /// ```
2683    /// use debian_control::lossless::relations::Relation;
2684    /// let relation: Relation = "samba <!nocheck>".parse().unwrap();
2685    /// let ranges: Vec<_> = relation.profile_ranges().collect();
2686    /// assert_eq!(ranges.len(), 1);
2687    /// let r = ranges[0];
2688    /// assert_eq!(&relation.to_string()[r], "nocheck");
2689    /// ```
2690    pub fn profile_ranges(&self) -> impl Iterator<Item = rowan::TextRange> + '_ {
2691        self.0
2692            .children()
2693            .filter(|n| n.kind() == PROFILES)
2694            .flat_map(|profile| {
2695                profile
2696                    .children_with_tokens()
2697                    .filter_map(|t| t.into_token())
2698                    .filter(|t| t.kind() == IDENT)
2699                    .map(|t| t.text_range())
2700                    .collect::<Vec<_>>()
2701            })
2702    }
2703
2704    /// Remove this relation
2705    ///
2706    /// # Example
2707    /// ```
2708    /// use debian_control::lossless::relations::{Relation,Entry};
2709    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-urllib3 (<< 1.26.0)".parse().unwrap();
2710    /// let mut relation = entry.get_relation(0).unwrap();
2711    /// relation.remove();
2712    /// assert_eq!(entry.to_string(), "python3-urllib3 (<< 1.26.0)");
2713    /// ```
2714    pub fn remove(&mut self) {
2715        let is_first = !self
2716            .0
2717            .siblings(Direction::Prev)
2718            .skip(1)
2719            .any(|n| n.kind() == RELATION);
2720        if !is_first {
2721            // Not the first item in the list. Remove whitespace backwards to the previous
2722            // pipe, the pipe and any whitespace until the previous relation
2723            while let Some(n) = self.0.prev_sibling_or_token() {
2724                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2725                    n.detach();
2726                } else if n.kind() == PIPE {
2727                    n.detach();
2728                    break;
2729                } else {
2730                    break;
2731                }
2732            }
2733            while let Some(n) = self.0.prev_sibling_or_token() {
2734                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2735                    n.detach();
2736                } else {
2737                    break;
2738                }
2739            }
2740        } else {
2741            // First item in the list. Remove whitespace up to the pipe, the pipe and anything
2742            // before the next relation
2743            while let Some(n) = self.0.next_sibling_or_token() {
2744                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2745                    n.detach();
2746                } else if n.kind() == PIPE {
2747                    n.detach();
2748                    break;
2749                } else {
2750                    break;
2751                }
2752            }
2753
2754            while let Some(n) = self.0.next_sibling_or_token() {
2755                if matches!(n.kind(), WHITESPACE | NEWLINE | COMMENT) {
2756                    n.detach();
2757                } else {
2758                    break;
2759                }
2760            }
2761        }
2762        // If this was the last relation in the entry, remove the entire entry
2763        if let Some(mut parent) = self.0.parent().and_then(Entry::cast) {
2764            if parent.is_empty() {
2765                parent.remove();
2766            } else {
2767                self.0.detach();
2768            }
2769        } else {
2770            self.0.detach();
2771        }
2772    }
2773
2774    /// Set the architectures for this relation
2775    ///
2776    /// # Example
2777    /// ```
2778    /// use debian_control::lossless::relations::Relation;
2779    /// let mut relation = Relation::simple("samba");
2780    /// relation.set_architectures(vec!["amd64", "i386"].into_iter());
2781    /// assert_eq!(relation.to_string(), "samba [amd64 i386]");
2782    /// ```
2783    pub fn set_architectures<'a>(&mut self, architectures: impl Iterator<Item = &'a str>) {
2784        let mut builder = GreenNodeBuilder::new();
2785        builder.start_node(ARCHITECTURES.into());
2786        builder.token(L_BRACKET.into(), "[");
2787        for (i, arch) in architectures.enumerate() {
2788            if i > 0 {
2789                builder.token(WHITESPACE.into(), " ");
2790            }
2791            builder.token(IDENT.into(), arch);
2792        }
2793        builder.token(R_BRACKET.into(), "]");
2794        builder.finish_node();
2795
2796        let node_architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES);
2797        if let Some(node_architectures) = node_architectures {
2798            let new_root = SyntaxNode::new_root_mut(builder.finish());
2799            self.0.splice_children(
2800                node_architectures.index()..node_architectures.index() + 1,
2801                vec![new_root.into()],
2802            );
2803        } else {
2804            let profiles = self.0.children().find(|n| n.kind() == PROFILES);
2805            let idx = if let Some(profiles) = profiles {
2806                profiles.index()
2807            } else {
2808                self.0.children_with_tokens().count()
2809            };
2810            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2811                idx..idx,
2812                vec![
2813                    GreenToken::new(WHITESPACE.into(), " ").into(),
2814                    builder.finish().into(),
2815                ],
2816            ));
2817            if let Some(parent) = self.0.parent() {
2818                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2819                self.0 = parent
2820                    .children_with_tokens()
2821                    .nth(self.0.index())
2822                    .unwrap()
2823                    .clone()
2824                    .into_node()
2825                    .unwrap();
2826            } else {
2827                self.0 = new_root;
2828            }
2829        }
2830    }
2831
2832    /// Add a build profile to this relation
2833    ///
2834    /// # Example
2835    /// ```
2836    /// use debian_control::lossless::relations::Relation;
2837    /// use debian_control::relations::BuildProfile;
2838    /// let mut relation = Relation::simple("samba");
2839    /// relation.add_profile(&[BuildProfile::Disabled("nocheck".to_string())]);
2840    /// assert_eq!(relation.to_string(), "samba <!nocheck>");
2841    /// ```
2842    pub fn add_profile(&mut self, profile: &[BuildProfile]) {
2843        let mut builder = GreenNodeBuilder::new();
2844        builder.start_node(PROFILES.into());
2845        builder.token(L_ANGLE.into(), "<");
2846        for (i, profile) in profile.iter().enumerate() {
2847            if i > 0 {
2848                builder.token(WHITESPACE.into(), " ");
2849            }
2850            match profile {
2851                BuildProfile::Disabled(name) => {
2852                    builder.token(NOT.into(), "!");
2853                    builder.token(IDENT.into(), name.as_str());
2854                }
2855                BuildProfile::Enabled(name) => {
2856                    builder.token(IDENT.into(), name.as_str());
2857                }
2858            }
2859        }
2860        builder.token(R_ANGLE.into(), ">");
2861        builder.finish_node();
2862
2863        let node_profiles = self.0.children().find(|n| n.kind() == PROFILES);
2864        if let Some(node_profiles) = node_profiles {
2865            let new_root = SyntaxNode::new_root_mut(builder.finish());
2866            self.0.splice_children(
2867                node_profiles.index()..node_profiles.index() + 1,
2868                vec![new_root.into()],
2869            );
2870        } else {
2871            let idx = self.0.children_with_tokens().count();
2872            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2873                idx..idx,
2874                vec![
2875                    GreenToken::new(WHITESPACE.into(), " ").into(),
2876                    builder.finish().into(),
2877                ],
2878            ));
2879            if let Some(parent) = self.0.parent() {
2880                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2881                self.0 = parent
2882                    .children_with_tokens()
2883                    .nth(self.0.index())
2884                    .unwrap()
2885                    .clone()
2886                    .into_node()
2887                    .unwrap();
2888            } else {
2889                self.0 = new_root;
2890            }
2891        }
2892    }
2893
2894    /// Build a new relation
2895    pub fn build(name: &str) -> RelationBuilder {
2896        RelationBuilder::new(name)
2897    }
2898
2899    /// Check if this relation is implied by another relation.
2900    ///
2901    /// A relation is implied by another if the outer relation is more restrictive
2902    /// or equal to this relation. For example:
2903    /// - `pkg >= 1.0` is implied by `pkg >= 1.5` (outer is more restrictive)
2904    /// - `pkg >= 1.0` is implied by `pkg = 1.5` (outer is more restrictive)
2905    /// - `pkg` (no version) is implied by any versioned constraint on `pkg`
2906    ///
2907    /// # Arguments
2908    /// * `outer` - The outer relation that may imply this relation
2909    ///
2910    /// # Returns
2911    /// `true` if this relation is implied by `outer`, `false` otherwise
2912    ///
2913    /// # Example
2914    /// ```
2915    /// use debian_control::lossless::relations::Relation;
2916    /// use debian_control::relations::VersionConstraint;
2917    ///
2918    /// let inner = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())));
2919    /// let outer = Relation::new("pkg", Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())));
2920    /// assert!(inner.is_implied_by(&outer));
2921    ///
2922    /// let inner2 = Relation::new("pkg", None);
2923    /// assert!(inner2.is_implied_by(&outer));
2924    /// ```
2925    pub fn is_implied_by(&self, outer: &Relation) -> bool {
2926        if self.try_name() != outer.try_name() {
2927            return false;
2928        }
2929
2930        let inner_version = self.version();
2931        let outer_version = outer.version();
2932
2933        // No version constraint on inner means it's always implied
2934        if inner_version.is_none() {
2935            return true;
2936        }
2937
2938        // If versions are identical, they imply each other
2939        if inner_version == outer_version {
2940            return true;
2941        }
2942
2943        // Inner has version but outer doesn't - not implied
2944        if outer_version.is_none() {
2945            return false;
2946        }
2947
2948        let (inner_constraint, inner_ver) = inner_version.unwrap();
2949        let (outer_constraint, outer_ver) = outer_version.unwrap();
2950
2951        use VersionConstraint::*;
2952        match inner_constraint {
2953            GreaterThanEqual => match outer_constraint {
2954                GreaterThan => outer_ver > inner_ver,
2955                GreaterThanEqual | Equal => outer_ver >= inner_ver,
2956                LessThan | LessThanEqual => false,
2957            },
2958            Equal => match outer_constraint {
2959                Equal => outer_ver == inner_ver,
2960                _ => false,
2961            },
2962            LessThan => match outer_constraint {
2963                LessThan => outer_ver <= inner_ver,
2964                LessThanEqual | Equal => outer_ver < inner_ver,
2965                GreaterThan | GreaterThanEqual => false,
2966            },
2967            LessThanEqual => match outer_constraint {
2968                LessThanEqual | Equal | LessThan => outer_ver <= inner_ver,
2969                GreaterThan | GreaterThanEqual => false,
2970            },
2971            GreaterThan => match outer_constraint {
2972                GreaterThan => outer_ver >= inner_ver,
2973                Equal | GreaterThanEqual => outer_ver > inner_ver,
2974                LessThan | LessThanEqual => false,
2975            },
2976        }
2977    }
2978}
2979
2980/// A builder for creating a `Relation`
2981///
2982/// # Example
2983/// ```
2984/// use debian_control::lossless::relations::{Relation};
2985/// use debian_control::relations::VersionConstraint;
2986/// let relation = Relation::build("samba")
2987///    .version_constraint(VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())
2988///    .archqual("any")
2989///    .architectures(vec!["amd64".to_string(), "i386".to_string()])
2990///    .build();
2991/// assert_eq!(relation.to_string(), "samba:any (>= 2.0) [amd64 i386]");
2992/// ```
2993pub struct RelationBuilder {
2994    name: String,
2995    version_constraint: Option<(VersionConstraint, Version)>,
2996    archqual: Option<String>,
2997    architectures: Option<Vec<String>>,
2998    profiles: Vec<Vec<BuildProfile>>,
2999}
3000
3001impl RelationBuilder {
3002    /// Create a new `RelationBuilder` with the given package name
3003    fn new(name: &str) -> Self {
3004        Self {
3005            name: name.to_string(),
3006            version_constraint: None,
3007            archqual: None,
3008            architectures: None,
3009            profiles: vec![],
3010        }
3011    }
3012
3013    /// Set the version constraint for this relation
3014    pub fn version_constraint(mut self, vc: VersionConstraint, version: Version) -> Self {
3015        self.version_constraint = Some((vc, version));
3016        self
3017    }
3018
3019    /// Set the architecture qualifier for this relation
3020    pub fn archqual(mut self, archqual: &str) -> Self {
3021        self.archqual = Some(archqual.to_string());
3022        self
3023    }
3024
3025    /// Set the architectures for this relation
3026    pub fn architectures(mut self, architectures: Vec<String>) -> Self {
3027        self.architectures = Some(architectures);
3028        self
3029    }
3030
3031    /// Set the build profiles for this relation
3032    pub fn profiles(mut self, profiles: Vec<Vec<BuildProfile>>) -> Self {
3033        self.profiles = profiles;
3034        self
3035    }
3036
3037    /// Add a build profile to this relation
3038    pub fn add_profile(mut self, profile: Vec<BuildProfile>) -> Self {
3039        self.profiles.push(profile);
3040        self
3041    }
3042
3043    /// Build the `Relation`
3044    pub fn build(self) -> Relation {
3045        let mut relation = Relation::new(&self.name, self.version_constraint);
3046        if let Some(archqual) = &self.archqual {
3047            relation.set_archqual(archqual);
3048        }
3049        if let Some(architectures) = &self.architectures {
3050            relation.set_architectures(architectures.iter().map(|s| s.as_str()));
3051        }
3052        for profile in &self.profiles {
3053            relation.add_profile(profile);
3054        }
3055        relation
3056    }
3057}
3058
3059impl PartialOrd for Relation {
3060    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3061        Some(self.cmp(other))
3062    }
3063}
3064
3065impl Eq for Relation {}
3066
3067impl Ord for Relation {
3068    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3069        // Compare by name first, then by version
3070        let name_cmp = self.try_name().cmp(&other.try_name());
3071        if name_cmp != std::cmp::Ordering::Equal {
3072            return name_cmp;
3073        }
3074
3075        let self_version = self.version();
3076        let other_version = other.version();
3077
3078        match (self_version, other_version) {
3079            (Some((self_vc, self_version)), Some((other_vc, other_version))) => {
3080                let vc_cmp = self_vc.cmp(&other_vc);
3081                if vc_cmp != std::cmp::Ordering::Equal {
3082                    return vc_cmp;
3083                }
3084
3085                self_version.cmp(&other_version)
3086            }
3087            (Some(_), None) => std::cmp::Ordering::Greater,
3088            (None, Some(_)) => std::cmp::Ordering::Less,
3089            (None, None) => std::cmp::Ordering::Equal,
3090        }
3091    }
3092}
3093
3094impl std::str::FromStr for Relations {
3095    type Err = String;
3096
3097    fn from_str(s: &str) -> Result<Self, Self::Err> {
3098        let parse = parse(s, false);
3099        if parse.errors.is_empty() {
3100            Ok(parse.root_mut())
3101        } else {
3102            Err(parse.errors.join("\n"))
3103        }
3104    }
3105}
3106
3107impl std::str::FromStr for Entry {
3108    type Err = String;
3109
3110    fn from_str(s: &str) -> Result<Self, Self::Err> {
3111        let root: Relations = s.parse()?;
3112
3113        let mut entries = root.entries();
3114        let entry = if let Some(entry) = entries.next() {
3115            entry
3116        } else {
3117            return Err("No entry found".to_string());
3118        };
3119
3120        if entries.next().is_some() {
3121            return Err("Multiple entries found".to_string());
3122        }
3123
3124        Ok(entry)
3125    }
3126}
3127
3128impl std::str::FromStr for Relation {
3129    type Err = String;
3130
3131    fn from_str(s: &str) -> Result<Self, Self::Err> {
3132        let entry: Entry = s.parse()?;
3133
3134        let mut relations = entry.relations();
3135        let relation = if let Some(relation) = relations.next() {
3136            relation
3137        } else {
3138            return Err("No relation found".to_string());
3139        };
3140
3141        if relations.next().is_some() {
3142            return Err("Multiple relations found".to_string());
3143        }
3144
3145        Ok(relation)
3146    }
3147}
3148
3149impl From<crate::lossy::Relation> for Relation {
3150    fn from(relation: crate::lossy::Relation) -> Self {
3151        let mut builder = Relation::build(&relation.name);
3152
3153        if let Some((vc, version)) = relation.version {
3154            builder = builder.version_constraint(vc, version);
3155        }
3156
3157        if let Some(archqual) = relation.archqual {
3158            builder = builder.archqual(&archqual);
3159        }
3160
3161        if let Some(architectures) = relation.architectures {
3162            builder = builder.architectures(architectures);
3163        }
3164
3165        builder = builder.profiles(relation.profiles);
3166
3167        builder.build()
3168    }
3169}
3170
3171impl From<Relation> for crate::lossy::Relation {
3172    fn from(relation: Relation) -> Self {
3173        crate::lossy::Relation {
3174            name: relation.try_name().unwrap_or_default(),
3175            version: relation.version(),
3176            archqual: relation.archqual(),
3177            architectures: relation.architectures().map(|a| a.collect()),
3178            profiles: relation.profiles().collect(),
3179        }
3180    }
3181}
3182
3183impl From<Entry> for Vec<crate::lossy::Relation> {
3184    fn from(entry: Entry) -> Self {
3185        entry.relations().map(|r| r.into()).collect()
3186    }
3187}
3188
3189impl From<Vec<crate::lossy::Relation>> for Entry {
3190    fn from(relations: Vec<crate::lossy::Relation>) -> Self {
3191        let relations: Vec<Relation> = relations.into_iter().map(|r| r.into()).collect();
3192        Entry::from(relations)
3193    }
3194}
3195
3196#[cfg(test)]
3197mod tests {
3198    use super::*;
3199
3200    #[test]
3201    fn test_parse() {
3202        let input = "python3-dulwich";
3203        let parsed: Relations = input.parse().unwrap();
3204        assert_eq!(parsed.to_string(), input);
3205        assert_eq!(parsed.entries().count(), 1);
3206        let entry = parsed.entries().next().unwrap();
3207        assert_eq!(entry.to_string(), "python3-dulwich");
3208        assert_eq!(entry.relations().count(), 1);
3209        let relation = entry.relations().next().unwrap();
3210        assert_eq!(relation.to_string(), "python3-dulwich");
3211        assert_eq!(relation.version(), None);
3212
3213        let input = "python3-dulwich (>= 0.20.21)";
3214        let parsed: Relations = input.parse().unwrap();
3215        assert_eq!(parsed.to_string(), input);
3216        assert_eq!(parsed.entries().count(), 1);
3217        let entry = parsed.entries().next().unwrap();
3218        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3219        assert_eq!(entry.relations().count(), 1);
3220        let relation = entry.relations().next().unwrap();
3221        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3222        assert_eq!(
3223            relation.version(),
3224            Some((
3225                VersionConstraint::GreaterThanEqual,
3226                "0.20.21".parse().unwrap()
3227            ))
3228        );
3229    }
3230
3231    #[test]
3232    fn test_multiple() {
3233        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
3234        let parsed: Relations = input.parse().unwrap();
3235        assert_eq!(parsed.to_string(), input);
3236        assert_eq!(parsed.entries().count(), 2);
3237        let entry = parsed.entries().next().unwrap();
3238        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3239        assert_eq!(entry.relations().count(), 1);
3240        let relation = entry.relations().next().unwrap();
3241        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
3242        assert_eq!(
3243            relation.version(),
3244            Some((
3245                VersionConstraint::GreaterThanEqual,
3246                "0.20.21".parse().unwrap()
3247            ))
3248        );
3249        let entry = parsed.entries().nth(1).unwrap();
3250        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.21)");
3251        assert_eq!(entry.relations().count(), 1);
3252        let relation = entry.relations().next().unwrap();
3253        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
3254        assert_eq!(
3255            relation.version(),
3256            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
3257        );
3258    }
3259
3260    #[test]
3261    fn test_architectures() {
3262        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
3263        let parsed: Relations = input.parse().unwrap();
3264        assert_eq!(parsed.to_string(), input);
3265        assert_eq!(parsed.entries().count(), 1);
3266        let entry = parsed.entries().next().unwrap();
3267        assert_eq!(
3268            entry.to_string(),
3269            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3270        );
3271        assert_eq!(entry.relations().count(), 1);
3272        let relation = entry.relations().next().unwrap();
3273        assert_eq!(
3274            relation.to_string(),
3275            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
3276        );
3277        assert_eq!(relation.version(), None);
3278        assert_eq!(
3279            relation.architectures().unwrap().collect::<Vec<_>>(),
3280            vec![
3281                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
3282            ]
3283            .into_iter()
3284            .map(|s| s.to_string())
3285            .collect::<Vec<_>>()
3286        );
3287    }
3288
3289    #[test]
3290    fn test_profiles() {
3291        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
3292        let parsed: Relations = input.parse().unwrap();
3293        assert_eq!(parsed.to_string(), input);
3294        assert_eq!(parsed.entries().count(), 2);
3295        let entry = parsed.entries().next().unwrap();
3296        assert_eq!(
3297            entry.to_string(),
3298            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3299        );
3300        assert_eq!(entry.relations().count(), 1);
3301        let relation = entry.relations().next().unwrap();
3302        assert_eq!(
3303            relation.to_string(),
3304            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
3305        );
3306        assert_eq!(
3307            relation.version(),
3308            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
3309        );
3310        assert_eq!(
3311            relation.architectures().unwrap().collect::<Vec<_>>(),
3312            vec!["i386", "arm"]
3313                .into_iter()
3314                .map(|s| s.to_string())
3315                .collect::<Vec<_>>()
3316        );
3317        assert_eq!(
3318            relation.profiles().collect::<Vec<_>>(),
3319            vec![
3320                vec![BuildProfile::Disabled("nocheck".to_string())],
3321                vec![BuildProfile::Disabled("cross".to_string())]
3322            ]
3323        );
3324    }
3325
3326    #[test]
3327    fn test_substvar() {
3328        let input = "${shlibs:Depends}";
3329
3330        let (parsed, errors) = Relations::parse_relaxed(input, true);
3331        assert_eq!(errors, Vec::<String>::new());
3332        assert_eq!(parsed.to_string(), input);
3333        assert_eq!(parsed.entries().count(), 0);
3334
3335        assert_eq!(
3336            parsed.substvars().collect::<Vec<_>>(),
3337            vec!["${shlibs:Depends}"]
3338        );
3339    }
3340
3341    #[test]
3342    fn test_substvar_nodes() {
3343        let input = "foo, ${shlibs:Depends}, bar, ${misc:Depends}";
3344
3345        let (parsed, errors) = Relations::parse_relaxed(input, true);
3346        assert_eq!(errors, Vec::<String>::new());
3347
3348        let substvar_nodes: Vec<Substvar> = parsed.substvar_nodes().collect();
3349        assert_eq!(substvar_nodes.len(), 2);
3350        assert_eq!(substvar_nodes[0].to_string(), "${shlibs:Depends}");
3351        assert_eq!(substvar_nodes[1].to_string(), "${misc:Depends}");
3352    }
3353
3354    #[test]
3355    fn test_substvar_nodes_empty() {
3356        let parsed: Relations = "foo, bar".parse().unwrap();
3357
3358        let substvar_nodes: Vec<Substvar> = parsed.substvar_nodes().collect();
3359        assert_eq!(substvar_nodes.len(), 0);
3360    }
3361
3362    #[test]
3363    fn test_new() {
3364        let r = Relation::new(
3365            "samba",
3366            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3367        );
3368
3369        assert_eq!(r.to_string(), "samba (>= 2.0)");
3370    }
3371
3372    #[test]
3373    fn test_drop_constraint() {
3374        let mut r = Relation::new(
3375            "samba",
3376            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
3377        );
3378
3379        r.drop_constraint();
3380
3381        assert_eq!(r.to_string(), "samba");
3382    }
3383
3384    #[test]
3385    fn test_simple() {
3386        let r = Relation::simple("samba");
3387
3388        assert_eq!(r.to_string(), "samba");
3389    }
3390
3391    #[test]
3392    fn test_remove_first_entry() {
3393        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3394            .parse()
3395            .unwrap();
3396        let removed = rels.remove_entry(0);
3397        assert_eq!(removed.to_string(), "python3-dulwich (>= 0.20.21)");
3398        assert_eq!(rels.to_string(), "python3-dulwich (<< 0.21)");
3399    }
3400
3401    #[test]
3402    fn test_remove_last_entry() {
3403        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3404            .parse()
3405            .unwrap();
3406        rels.remove_entry(1);
3407        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3408    }
3409
3410    #[test]
3411    fn test_remove_middle() {
3412        let mut rels: Relations =
3413            r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21), python3-dulwich (<< 0.22)"#
3414                .parse()
3415                .unwrap();
3416        rels.remove_entry(1);
3417        assert_eq!(
3418            rels.to_string(),
3419            "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.22)"
3420        );
3421    }
3422
3423    #[test]
3424    fn test_remove_added() {
3425        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3426        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3427        rels.push(entry);
3428        rels.remove_entry(1);
3429        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3430    }
3431
3432    #[test]
3433    fn test_push() {
3434        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3435        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3436        rels.push(entry);
3437        assert_eq!(
3438            rels.to_string(),
3439            "python3-dulwich (>= 0.20.21), python3-dulwich"
3440        );
3441    }
3442
3443    #[test]
3444    fn test_insert_with_custom_separator() {
3445        let mut rels: Relations = "python3".parse().unwrap();
3446        let entry = Entry::from(vec![Relation::simple("debhelper")]);
3447        rels.insert_with_separator(1, entry, Some("\n "));
3448        assert_eq!(rels.to_string(), "python3,\n debhelper");
3449    }
3450
3451    #[test]
3452    fn test_insert_with_wrap_and_sort_separator() {
3453        let mut rels: Relations = "python3".parse().unwrap();
3454        let entry = Entry::from(vec![Relation::simple("rustc")]);
3455        // Simulate wrap-and-sort -a style with field name "Depends: " (9 chars)
3456        rels.insert_with_separator(1, entry, Some("\n         "));
3457        assert_eq!(rels.to_string(), "python3,\n         rustc");
3458    }
3459
3460    #[test]
3461    fn test_push_from_empty() {
3462        let mut rels: Relations = "".parse().unwrap();
3463        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3464        rels.push(entry);
3465        assert_eq!(rels.to_string(), "python3-dulwich");
3466    }
3467
3468    #[test]
3469    fn test_insert() {
3470        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3471            .parse()
3472            .unwrap();
3473        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3474        rels.insert(1, entry);
3475        assert_eq!(
3476            rels.to_string(),
3477            "python3-dulwich (>= 0.20.21), python3-dulwich, python3-dulwich (<< 0.21)"
3478        );
3479    }
3480
3481    #[test]
3482    fn test_insert_at_start() {
3483        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3484            .parse()
3485            .unwrap();
3486        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3487        rels.insert(0, entry);
3488        assert_eq!(
3489            rels.to_string(),
3490            "python3-dulwich, python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3491        );
3492    }
3493
3494    #[test]
3495    fn test_insert_after_error() {
3496        let (mut rels, errors) = Relations::parse_relaxed("@foo@, debhelper (>= 1.0)", false);
3497        assert_eq!(
3498            errors,
3499            vec![
3500                "expected $ or identifier but got ERROR",
3501                "expected comma or end of file but got Some(IDENT)",
3502                "expected $ or identifier but got ERROR"
3503            ]
3504        );
3505        let entry = Entry::from(vec![Relation::simple("bar")]);
3506        rels.push(entry);
3507        assert_eq!(rels.to_string(), "@foo@, debhelper (>= 1.0), bar");
3508    }
3509
3510    #[test]
3511    fn test_insert_before_error() {
3512        let (mut rels, errors) = Relations::parse_relaxed("debhelper (>= 1.0), @foo@, bla", false);
3513        assert_eq!(
3514            errors,
3515            vec![
3516                "expected $ or identifier but got ERROR",
3517                "expected comma or end of file but got Some(IDENT)",
3518                "expected $ or identifier but got ERROR"
3519            ]
3520        );
3521        let entry = Entry::from(vec![Relation::simple("bar")]);
3522        rels.insert(0, entry);
3523        assert_eq!(rels.to_string(), "bar, debhelper (>= 1.0), @foo@, bla");
3524    }
3525
3526    #[test]
3527    fn test_replace() {
3528        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3529            .parse()
3530            .unwrap();
3531        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3532        rels.replace(1, entry);
3533        assert_eq!(
3534            rels.to_string(),
3535            "python3-dulwich (>= 0.20.21), python3-dulwich"
3536        );
3537    }
3538
3539    #[test]
3540    fn test_relation_from_entries() {
3541        let entries = vec![
3542            Entry::from(vec![Relation::simple("python3-dulwich")]),
3543            Entry::from(vec![Relation::simple("python3-breezy")]),
3544        ];
3545        let rels: Relations = entries.into();
3546        assert_eq!(rels.entries().count(), 2);
3547        assert_eq!(rels.to_string(), "python3-dulwich, python3-breezy");
3548    }
3549
3550    #[test]
3551    fn test_entry_from_relations() {
3552        let relations = vec![
3553            Relation::simple("python3-dulwich"),
3554            Relation::simple("python3-breezy"),
3555        ];
3556        let entry: Entry = relations.into();
3557        assert_eq!(entry.relations().count(), 2);
3558        assert_eq!(entry.to_string(), "python3-dulwich | python3-breezy");
3559    }
3560
3561    #[test]
3562    fn test_parse_entry() {
3563        let parsed: Entry = "python3-dulwich (>= 0.20.21) | bar".parse().unwrap();
3564        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21) | bar");
3565        assert_eq!(parsed.relations().count(), 2);
3566
3567        assert_eq!(
3568            "foo, bar".parse::<Entry>().unwrap_err(),
3569            "Multiple entries found"
3570        );
3571        assert_eq!("".parse::<Entry>().unwrap_err(), "No entry found");
3572    }
3573
3574    #[test]
3575    fn test_parse_relation() {
3576        let parsed: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3577        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
3578        assert_eq!(
3579            parsed.version(),
3580            Some((
3581                VersionConstraint::GreaterThanEqual,
3582                "0.20.21".parse().unwrap()
3583            ))
3584        );
3585        assert_eq!(
3586            "foo | bar".parse::<Relation>().unwrap_err(),
3587            "Multiple relations found"
3588        );
3589        assert_eq!("".parse::<Relation>().unwrap_err(), "No entry found");
3590    }
3591
3592    #[test]
3593    fn test_special() {
3594        let parsed: Relation = "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3595            .parse()
3596            .unwrap();
3597        assert_eq!(
3598            parsed.to_string(),
3599            "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3600        );
3601        assert_eq!(
3602            parsed.version(),
3603            Some((
3604                VersionConstraint::GreaterThanEqual,
3605                "0.1.138-~~".parse().unwrap()
3606            ))
3607        );
3608        assert_eq!(parsed.archqual(), Some("amd64".to_string()));
3609        assert_eq!(parsed.name(), "librust-breezyshim+dirty-tracker-dev");
3610    }
3611
3612    #[test]
3613    fn test_relations_satisfied_by() {
3614        let rels: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3615            .parse()
3616            .unwrap();
3617        let satisfied = |name: &str| -> Option<debversion::Version> {
3618            match name {
3619                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3620                _ => None,
3621            }
3622        };
3623        assert!(rels.satisfied_by(satisfied));
3624
3625        let satisfied = |name: &str| match name {
3626            "python3-dulwich" => Some("0.21".parse().unwrap()),
3627            _ => None,
3628        };
3629        assert!(!rels.satisfied_by(satisfied));
3630
3631        let satisfied = |name: &str| match name {
3632            "python3-dulwich" => Some("0.20.20".parse().unwrap()),
3633            _ => None,
3634        };
3635        assert!(!rels.satisfied_by(satisfied));
3636    }
3637
3638    #[test]
3639    fn test_entry_satisfied_by() {
3640        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3641            .parse()
3642            .unwrap();
3643        let satisfied = |name: &str| -> Option<debversion::Version> {
3644            match name {
3645                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3646                _ => None,
3647            }
3648        };
3649        assert!(entry.satisfied_by(satisfied));
3650        let satisfied = |name: &str| -> Option<debversion::Version> {
3651            match name {
3652                "python3-dulwich" => Some("0.18".parse().unwrap()),
3653                _ => None,
3654            }
3655        };
3656        assert!(!entry.satisfied_by(satisfied));
3657    }
3658
3659    #[test]
3660    fn test_wrap_and_sort_relation() {
3661        let relation: Relation = "   python3-dulwich   (>= 11) [  amd64 ] <  lala>"
3662            .parse()
3663            .unwrap();
3664
3665        let wrapped = relation.wrap_and_sort();
3666
3667        assert_eq!(
3668            wrapped.to_string(),
3669            "python3-dulwich (>= 11) [amd64] <lala>"
3670        );
3671    }
3672
3673    #[test]
3674    fn test_wrap_and_sort_relations() {
3675        let entry: Relations =
3676            "python3-dulwich (>= 0.20.21)   | bar, \n\n\n\npython3-dulwich (<< 0.21)"
3677                .parse()
3678                .unwrap();
3679
3680        let wrapped = entry.wrap_and_sort();
3681
3682        assert_eq!(
3683            wrapped.to_string(),
3684            "bar | python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3685        );
3686    }
3687
3688    #[cfg(feature = "serde")]
3689    #[test]
3690    fn test_serialize_relations() {
3691        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3692            .parse()
3693            .unwrap();
3694        let serialized = serde_json::to_string(&relations).unwrap();
3695        assert_eq!(
3696            serialized,
3697            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
3698        );
3699    }
3700
3701    #[cfg(feature = "serde")]
3702    #[test]
3703    fn test_deserialize_relations() {
3704        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3705            .parse()
3706            .unwrap();
3707        let serialized = serde_json::to_string(&relations).unwrap();
3708        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
3709        assert_eq!(deserialized.to_string(), relations.to_string());
3710    }
3711
3712    #[cfg(feature = "serde")]
3713    #[test]
3714    fn test_serialize_relation() {
3715        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3716        let serialized = serde_json::to_string(&relation).unwrap();
3717        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
3718    }
3719
3720    #[cfg(feature = "serde")]
3721    #[test]
3722    fn test_deserialize_relation() {
3723        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3724        let serialized = serde_json::to_string(&relation).unwrap();
3725        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
3726        assert_eq!(deserialized.to_string(), relation.to_string());
3727    }
3728
3729    #[cfg(feature = "serde")]
3730    #[test]
3731    fn test_serialize_entry() {
3732        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3733            .parse()
3734            .unwrap();
3735        let serialized = serde_json::to_string(&entry).unwrap();
3736        assert_eq!(
3737            serialized,
3738            r#""python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)""#
3739        );
3740    }
3741
3742    #[cfg(feature = "serde")]
3743    #[test]
3744    fn test_deserialize_entry() {
3745        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3746            .parse()
3747            .unwrap();
3748        let serialized = serde_json::to_string(&entry).unwrap();
3749        let deserialized: Entry = serde_json::from_str(&serialized).unwrap();
3750        assert_eq!(deserialized.to_string(), entry.to_string());
3751    }
3752
3753    #[test]
3754    fn test_remove_first_relation() {
3755        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3756            .parse()
3757            .unwrap();
3758        let mut rel = entry.relations().next().unwrap();
3759        rel.remove();
3760        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.18)");
3761    }
3762
3763    #[test]
3764    fn test_remove_last_relation() {
3765        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3766            .parse()
3767            .unwrap();
3768        let mut rel = entry.relations().nth(1).unwrap();
3769        rel.remove();
3770        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3771    }
3772
3773    #[test]
3774    fn test_remove_only_relation() {
3775        let entry: Entry = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3776        let mut rel = entry.relations().next().unwrap();
3777        rel.remove();
3778        assert_eq!(entry.to_string(), "");
3779    }
3780
3781    #[test]
3782    fn test_relations_is_empty() {
3783        let entry: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3784        assert!(!entry.is_empty());
3785        assert_eq!(1, entry.len());
3786        let mut rel = entry.entries().next().unwrap();
3787        rel.remove();
3788        assert!(entry.is_empty());
3789        assert_eq!(0, entry.len());
3790    }
3791
3792    #[test]
3793    fn test_entry_is_empty() {
3794        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3795            .parse()
3796            .unwrap();
3797        assert!(!entry.is_empty());
3798        assert_eq!(2, entry.len());
3799        let mut rel = entry.relations().next().unwrap();
3800        rel.remove();
3801        assert!(!entry.is_empty());
3802        assert_eq!(1, entry.len());
3803        let mut rel = entry.relations().next().unwrap();
3804        rel.remove();
3805        assert!(entry.is_empty());
3806        assert_eq!(0, entry.len());
3807    }
3808
3809    #[test]
3810    fn test_relation_set_version() {
3811        let mut rel: Relation = "samba".parse().unwrap();
3812        rel.set_version(None);
3813        assert_eq!("samba", rel.to_string());
3814        rel.set_version(Some((
3815            VersionConstraint::GreaterThanEqual,
3816            "2.0".parse().unwrap(),
3817        )));
3818        assert_eq!("samba (>= 2.0)", rel.to_string());
3819        rel.set_version(None);
3820        assert_eq!("samba", rel.to_string());
3821        rel.set_version(Some((
3822            VersionConstraint::GreaterThanEqual,
3823            "2.0".parse().unwrap(),
3824        )));
3825        rel.set_version(Some((
3826            VersionConstraint::GreaterThanEqual,
3827            "1.1".parse().unwrap(),
3828        )));
3829        assert_eq!("samba (>= 1.1)", rel.to_string());
3830    }
3831
3832    #[test]
3833    fn test_replace_relation() {
3834        let mut entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3835            .parse()
3836            .unwrap();
3837        let new_rel = Relation::simple("python3-breezy");
3838        entry.replace(0, new_rel);
3839        assert_eq!(
3840            entry.to_string(),
3841            "python3-breezy | python3-dulwich (<< 0.18)"
3842        );
3843    }
3844
3845    #[test]
3846    fn test_entry_push_relation() {
3847        let relations: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3848        let new_rel = Relation::simple("python3-breezy");
3849        let mut entry = relations.entries().next().unwrap();
3850        entry.push(new_rel);
3851        assert_eq!(
3852            entry.to_string(),
3853            "python3-dulwich (>= 0.20.21) | python3-breezy"
3854        );
3855        assert_eq!(
3856            relations.to_string(),
3857            "python3-dulwich (>= 0.20.21) | python3-breezy"
3858        );
3859    }
3860
3861    #[test]
3862    fn test_relations_remove_empty_entry() {
3863        let (mut relations, errors) = Relations::parse_relaxed("foo, , bar, ", false);
3864        assert_eq!(errors, Vec::<String>::new());
3865        assert_eq!(relations.to_string(), "foo, , bar, ");
3866        assert_eq!(relations.len(), 2);
3867        assert_eq!(
3868            relations.entries().next().unwrap().to_string(),
3869            "foo".to_string()
3870        );
3871        assert_eq!(
3872            relations.entries().nth(1).unwrap().to_string(),
3873            "bar".to_string()
3874        );
3875        relations.remove_entry(1);
3876        assert_eq!(relations.to_string(), "foo, , ");
3877    }
3878
3879    #[test]
3880    fn test_entry_remove_relation() {
3881        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3882        let removed = entry.remove_relation(0);
3883        assert_eq!(removed.to_string(), "python3-dulwich");
3884        assert_eq!(entry.to_string(), "samba");
3885    }
3886
3887    #[test]
3888    fn test_wrap_and_sort_removes_empty_entries() {
3889        let relations: Relations = "foo, , bar, ".parse().unwrap();
3890        let wrapped = relations.wrap_and_sort();
3891        assert_eq!(wrapped.to_string(), "bar, foo");
3892    }
3893
3894    #[test]
3895    fn test_set_archqual() {
3896        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3897        let mut rel = entry.relations().next().unwrap();
3898        rel.set_archqual("amd64");
3899        assert_eq!(rel.to_string(), "python3-dulwich:amd64");
3900        assert_eq!(rel.archqual(), Some("amd64".to_string()));
3901        assert_eq!(entry.to_string(), "python3-dulwich:amd64 | samba");
3902        rel.set_archqual("i386");
3903        assert_eq!(rel.to_string(), "python3-dulwich:i386");
3904        assert_eq!(rel.archqual(), Some("i386".to_string()));
3905        assert_eq!(entry.to_string(), "python3-dulwich:i386 | samba");
3906    }
3907
3908    #[test]
3909    fn test_set_architectures() {
3910        let mut relation = Relation::simple("samba");
3911        relation.set_architectures(vec!["amd64", "i386"].into_iter());
3912        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3913    }
3914
3915    #[test]
3916    fn test_relation_builder_no_architectures() {
3917        // Test that building a relation without architectures doesn't add empty brackets
3918        let relation = Relation::build("debhelper").build();
3919        assert_eq!(relation.to_string(), "debhelper");
3920    }
3921
3922    #[test]
3923    fn test_relation_builder_with_architectures() {
3924        // Test that building a relation with architectures works correctly
3925        let relation = Relation::build("samba")
3926            .architectures(vec!["amd64".to_string(), "i386".to_string()])
3927            .build();
3928        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3929    }
3930
3931    #[test]
3932    fn test_ensure_minimum_version_add_new() {
3933        let mut relations: Relations = "python3".parse().unwrap();
3934        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3935        assert_eq!(relations.to_string(), "python3, debhelper (>= 12)");
3936    }
3937
3938    #[test]
3939    fn test_ensure_minimum_version_update() {
3940        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3941        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3942        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3943    }
3944
3945    #[test]
3946    fn test_ensure_minimum_version_no_change() {
3947        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
3948        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3949        assert_eq!(relations.to_string(), "debhelper (>= 13)");
3950    }
3951
3952    #[test]
3953    fn test_ensure_minimum_version_no_version() {
3954        let mut relations: Relations = "debhelper".parse().unwrap();
3955        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3956        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3957    }
3958
3959    #[test]
3960    fn test_ensure_minimum_version_preserves_newline() {
3961        // Test that newline after the field name is preserved
3962        // This is the format often used in Debian control files:
3963        // Build-Depends:
3964        //  debhelper (>= 9),
3965        //  pkg-config
3966        let input = "\n debhelper (>= 9),\n pkg-config,\n uuid-dev";
3967        let mut relations: Relations = input.parse().unwrap();
3968        relations.ensure_minimum_version("debhelper", &"12~".parse().unwrap());
3969        let result = relations.to_string();
3970
3971        // The newline before the first entry should be preserved
3972        assert!(
3973            result.starts_with('\n'),
3974            "Expected result to start with newline, got: {:?}",
3975            result
3976        );
3977        assert_eq!(result, "\n debhelper (>= 12~),\n pkg-config,\n uuid-dev");
3978    }
3979
3980    #[test]
3981    fn test_ensure_minimum_version_preserves_newline_in_control() {
3982        // Test the full scenario from the bug report
3983        use crate::lossless::Control;
3984        use std::str::FromStr;
3985
3986        let input = r#"Source: f2fs-tools
3987Section: admin
3988Priority: optional
3989Maintainer: Test <test@example.com>
3990Build-Depends:
3991 debhelper (>= 9),
3992 pkg-config,
3993 uuid-dev
3994
3995Package: f2fs-tools
3996Description: test
3997"#;
3998
3999        let control = Control::from_str(input).unwrap();
4000        let mut source = control.source().unwrap();
4001        let mut build_depends = source.build_depends().unwrap();
4002
4003        let version = Version::from_str("12~").unwrap();
4004        build_depends.ensure_minimum_version("debhelper", &version);
4005
4006        source.set_build_depends(&build_depends);
4007
4008        let output = control.to_string();
4009
4010        // Check that the formatting is preserved - the newline after "Build-Depends:" should still be there
4011        assert!(
4012            output.contains("Build-Depends:\n debhelper (>= 12~)"),
4013            "Expected 'Build-Depends:\\n debhelper (>= 12~)' but got:\n{}",
4014            output
4015        );
4016    }
4017
4018    #[test]
4019    fn test_ensure_exact_version_add_new() {
4020        let mut relations: Relations = "python3".parse().unwrap();
4021        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
4022        assert_eq!(relations.to_string(), "python3, debhelper (= 12)");
4023    }
4024
4025    #[test]
4026    fn test_ensure_exact_version_update() {
4027        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
4028        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
4029        assert_eq!(relations.to_string(), "debhelper (= 12)");
4030    }
4031
4032    #[test]
4033    fn test_ensure_exact_version_no_change() {
4034        let mut relations: Relations = "debhelper (= 12)".parse().unwrap();
4035        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
4036        assert_eq!(relations.to_string(), "debhelper (= 12)");
4037    }
4038
4039    #[test]
4040    fn test_ensure_some_version_add_new() {
4041        let mut relations: Relations = "python3".parse().unwrap();
4042        relations.ensure_some_version("debhelper");
4043        assert_eq!(relations.to_string(), "python3, debhelper");
4044    }
4045
4046    #[test]
4047    fn test_ensure_some_version_exists_with_version() {
4048        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
4049        relations.ensure_some_version("debhelper");
4050        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4051    }
4052
4053    #[test]
4054    fn test_ensure_some_version_exists_no_version() {
4055        let mut relations: Relations = "debhelper".parse().unwrap();
4056        relations.ensure_some_version("debhelper");
4057        assert_eq!(relations.to_string(), "debhelper");
4058    }
4059
4060    #[test]
4061    fn test_ensure_substvar() {
4062        let mut relations: Relations = "python3".parse().unwrap();
4063        relations.ensure_substvar("${misc:Depends}").unwrap();
4064        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
4065    }
4066
4067    #[test]
4068    fn test_ensure_substvar_already_exists() {
4069        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
4070        relations.ensure_substvar("${misc:Depends}").unwrap();
4071        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
4072    }
4073
4074    #[test]
4075    fn test_ensure_substvar_empty_relations() {
4076        let mut relations: Relations = Relations::new();
4077        relations.ensure_substvar("${misc:Depends}").unwrap();
4078        assert_eq!(relations.to_string(), "${misc:Depends}");
4079    }
4080
4081    #[test]
4082    fn test_ensure_substvar_preserves_whitespace() {
4083        // Test with non-standard whitespace (multiple spaces)
4084        let (mut relations, _) = Relations::parse_relaxed("python3,  rustc", false);
4085        relations.ensure_substvar("${misc:Depends}").unwrap();
4086        // Should preserve the double-space pattern
4087        assert_eq!(relations.to_string(), "python3,  rustc,  ${misc:Depends}");
4088    }
4089
4090    #[test]
4091    fn test_ensure_substvar_to_existing_substvar() {
4092        // Test adding a substvar to existing substvar (no entries)
4093        // This reproduces the bug where space after comma is lost
4094        let (mut relations, _) = Relations::parse_relaxed("${shlibs:Depends}", true);
4095        relations.ensure_substvar("${misc:Depends}").unwrap();
4096        // Should have a space after the comma
4097        assert_eq!(relations.to_string(), "${shlibs:Depends}, ${misc:Depends}");
4098    }
4099
4100    #[test]
4101    fn test_drop_substvar_basic() {
4102        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
4103        relations.drop_substvar("${misc:Depends}");
4104        assert_eq!(relations.to_string(), "python3");
4105    }
4106
4107    #[test]
4108    fn test_drop_substvar_first_position() {
4109        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}, python3", true);
4110        relations.drop_substvar("${misc:Depends}");
4111        assert_eq!(relations.to_string(), "python3");
4112    }
4113
4114    #[test]
4115    fn test_drop_substvar_middle_position() {
4116        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
4117        relations.drop_substvar("${misc:Depends}");
4118        assert_eq!(relations.to_string(), "python3, rustc");
4119    }
4120
4121    #[test]
4122    fn test_drop_substvar_only_substvar() {
4123        let (mut relations, _) = Relations::parse_relaxed("${misc:Depends}", true);
4124        relations.drop_substvar("${misc:Depends}");
4125        assert_eq!(relations.to_string(), "");
4126    }
4127
4128    #[test]
4129    fn test_drop_substvar_not_exists() {
4130        let (mut relations, _) = Relations::parse_relaxed("python3, rustc", false);
4131        relations.drop_substvar("${misc:Depends}");
4132        assert_eq!(relations.to_string(), "python3, rustc");
4133    }
4134
4135    #[test]
4136    fn test_drop_substvar_multiple_substvars() {
4137        let (mut relations, _) =
4138            Relations::parse_relaxed("python3, ${misc:Depends}, ${shlibs:Depends}", true);
4139        relations.drop_substvar("${misc:Depends}");
4140        assert_eq!(relations.to_string(), "python3, ${shlibs:Depends}");
4141    }
4142
4143    #[test]
4144    fn test_drop_substvar_preserves_whitespace() {
4145        let (mut relations, _) = Relations::parse_relaxed("python3,  ${misc:Depends}", true);
4146        relations.drop_substvar("${misc:Depends}");
4147        assert_eq!(relations.to_string(), "python3");
4148    }
4149
4150    #[test]
4151    fn test_filter_entries_basic() {
4152        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4153        relations.filter_entries(|entry| entry.relations().any(|r| r.name().starts_with("python")));
4154        assert_eq!(relations.to_string(), "python3");
4155    }
4156
4157    #[test]
4158    fn test_filter_entries_keep_all() {
4159        let mut relations: Relations = "python3, debhelper".parse().unwrap();
4160        relations.filter_entries(|_| true);
4161        assert_eq!(relations.to_string(), "python3, debhelper");
4162    }
4163
4164    #[test]
4165    fn test_filter_entries_remove_all() {
4166        let mut relations: Relations = "python3, debhelper".parse().unwrap();
4167        relations.filter_entries(|_| false);
4168        assert_eq!(relations.to_string(), "");
4169    }
4170
4171    #[test]
4172    fn test_filter_entries_keep_middle() {
4173        let mut relations: Relations = "aaa, bbb, ccc".parse().unwrap();
4174        relations.filter_entries(|entry| entry.relations().any(|r| r.name() == "bbb"));
4175        assert_eq!(relations.to_string(), "bbb");
4176    }
4177
4178    // Tests for new convenience methods
4179
4180    #[test]
4181    fn test_is_sorted_wrap_and_sort_order() {
4182        // Sorted according to WrapAndSortOrder
4183        let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
4184        assert!(relations.is_sorted(&WrapAndSortOrder));
4185
4186        // Not sorted
4187        let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
4188        assert!(!relations.is_sorted(&WrapAndSortOrder));
4189
4190        // Build systems first (sorted alphabetically within their group)
4191        let (relations, _) =
4192            Relations::parse_relaxed("cdbs, debhelper-compat, python3, ${misc:Depends}", true);
4193        assert!(relations.is_sorted(&WrapAndSortOrder));
4194    }
4195
4196    #[test]
4197    fn test_is_sorted_default_order() {
4198        // Sorted alphabetically
4199        let relations: Relations = "aaa, bbb, ccc".parse().unwrap();
4200        assert!(relations.is_sorted(&DefaultSortingOrder));
4201
4202        // Not sorted
4203        let relations: Relations = "ccc, aaa, bbb".parse().unwrap();
4204        assert!(!relations.is_sorted(&DefaultSortingOrder));
4205
4206        // Special items at end
4207        let (relations, _) = Relations::parse_relaxed("aaa, bbb, ${misc:Depends}", true);
4208        assert!(relations.is_sorted(&DefaultSortingOrder));
4209    }
4210
4211    #[test]
4212    fn test_is_sorted_with_substvars() {
4213        // Substvars should be ignored by DefaultSortingOrder
4214        let (relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
4215        // This is considered sorted because ${misc:Depends} is ignored
4216        assert!(relations.is_sorted(&DefaultSortingOrder));
4217    }
4218
4219    #[test]
4220    fn test_drop_dependency_exists() {
4221        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4222        assert!(relations.drop_dependency("debhelper"));
4223        assert_eq!(relations.to_string(), "python3, rustc");
4224    }
4225
4226    #[test]
4227    fn test_drop_dependency_not_exists() {
4228        let mut relations: Relations = "python3, rustc".parse().unwrap();
4229        assert!(!relations.drop_dependency("nonexistent"));
4230        assert_eq!(relations.to_string(), "python3, rustc");
4231    }
4232
4233    #[test]
4234    fn test_drop_dependency_only_item() {
4235        let mut relations: Relations = "python3".parse().unwrap();
4236        assert!(relations.drop_dependency("python3"));
4237        assert_eq!(relations.to_string(), "");
4238    }
4239
4240    #[test]
4241    fn test_add_dependency_to_empty() {
4242        let mut relations: Relations = "".parse().unwrap();
4243        let entry = Entry::from(Relation::simple("python3"));
4244        relations.add_dependency(entry, None);
4245        assert_eq!(relations.to_string(), "python3");
4246    }
4247
4248    #[test]
4249    fn test_add_dependency_sorted_position() {
4250        let mut relations: Relations = "debhelper, rustc".parse().unwrap();
4251        let entry = Entry::from(Relation::simple("python3"));
4252        relations.add_dependency(entry, None);
4253        // Should be inserted in sorted position
4254        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4255    }
4256
4257    #[test]
4258    fn test_add_dependency_explicit_position() {
4259        let mut relations: Relations = "python3, rustc".parse().unwrap();
4260        let entry = Entry::from(Relation::simple("debhelper"));
4261        relations.add_dependency(entry, Some(0));
4262        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
4263    }
4264
4265    #[test]
4266    fn test_add_dependency_build_system_first() {
4267        let mut relations: Relations = "python3, rustc".parse().unwrap();
4268        let entry = Entry::from(Relation::simple("debhelper-compat"));
4269        relations.add_dependency(entry, None);
4270        // debhelper-compat should be inserted first (build system)
4271        assert_eq!(relations.to_string(), "debhelper-compat, python3, rustc");
4272    }
4273
4274    #[test]
4275    fn test_add_dependency_at_end() {
4276        let mut relations: Relations = "debhelper, python3".parse().unwrap();
4277        let entry = Entry::from(Relation::simple("zzz-package"));
4278        relations.add_dependency(entry, None);
4279        // Should be added at the end (alphabetically after python3)
4280        assert_eq!(relations.to_string(), "debhelper, python3, zzz-package");
4281    }
4282
4283    #[test]
4284    fn test_add_dependency_to_single_entry() {
4285        // Regression test: ensure comma is added when inserting into single-entry Relations
4286        let mut relations: Relations = "python3-dulwich".parse().unwrap();
4287        let entry: Entry = "debhelper-compat (= 12)".parse().unwrap();
4288        relations.add_dependency(entry, None);
4289        // Should insert with comma separator
4290        assert_eq!(
4291            relations.to_string(),
4292            "debhelper-compat (= 12), python3-dulwich"
4293        );
4294    }
4295
4296    #[test]
4297    fn test_get_relation_exists() {
4298        let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
4299        let result = relations.get_relation("debhelper");
4300        assert!(result.is_ok());
4301        let (idx, entry) = result.unwrap();
4302        assert_eq!(idx, 1);
4303        assert_eq!(entry.to_string(), "debhelper (>= 12)");
4304    }
4305
4306    #[test]
4307    fn test_get_relation_not_exists() {
4308        let relations: Relations = "python3, rustc".parse().unwrap();
4309        let result = relations.get_relation("nonexistent");
4310        assert_eq!(result, Err("Package nonexistent not found".to_string()));
4311    }
4312
4313    #[test]
4314    fn test_get_relation_complex_rule() {
4315        let relations: Relations = "python3 | python3-minimal, rustc".parse().unwrap();
4316        let result = relations.get_relation("python3");
4317        assert_eq!(
4318            result,
4319            Err("Complex rule for python3, aborting".to_string())
4320        );
4321    }
4322
4323    #[test]
4324    fn test_iter_relations_for_simple() {
4325        let relations: Relations = "python3, debhelper, python3-dev".parse().unwrap();
4326        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4327        assert_eq!(entries.len(), 1);
4328        assert_eq!(entries[0].0, 0);
4329        assert_eq!(entries[0].1.to_string(), "python3");
4330    }
4331
4332    #[test]
4333    fn test_iter_relations_for_alternatives() {
4334        let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
4335        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
4336        // Should find both the alternative entry and python3-dev is not included
4337        assert_eq!(entries.len(), 1);
4338        assert_eq!(entries[0].0, 0);
4339    }
4340
4341    #[test]
4342    fn test_iter_relations_for_not_found() {
4343        let relations: Relations = "python3, rustc".parse().unwrap();
4344        let entries: Vec<_> = relations.iter_relations_for("debhelper").collect();
4345        assert_eq!(entries.len(), 0);
4346    }
4347
4348    #[test]
4349    fn test_has_relation_exists() {
4350        let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
4351        assert!(relations.has_relation("debhelper"));
4352        assert!(relations.has_relation("python3"));
4353        assert!(relations.has_relation("rustc"));
4354    }
4355
4356    #[test]
4357    fn test_has_relation_not_exists() {
4358        let relations: Relations = "python3, rustc".parse().unwrap();
4359        assert!(!relations.has_relation("debhelper"));
4360    }
4361
4362    #[test]
4363    fn test_has_relation_in_alternative() {
4364        let relations: Relations = "python3 | python3-minimal".parse().unwrap();
4365        assert!(relations.has_relation("python3"));
4366        assert!(relations.has_relation("python3-minimal"));
4367    }
4368
4369    #[test]
4370    fn test_sorting_order_wrap_and_sort_build_systems() {
4371        let order = WrapAndSortOrder;
4372        // Build systems should come before regular packages
4373        assert!(order.lt("debhelper", "python3"));
4374        assert!(order.lt("debhelper-compat", "rustc"));
4375        assert!(order.lt("cdbs", "aaa"));
4376        assert!(order.lt("dh-python", "python3"));
4377    }
4378
4379    #[test]
4380    fn test_sorting_order_wrap_and_sort_regular_packages() {
4381        let order = WrapAndSortOrder;
4382        // Regular packages sorted alphabetically
4383        assert!(order.lt("aaa", "bbb"));
4384        assert!(order.lt("python3", "rustc"));
4385        assert!(!order.lt("rustc", "python3"));
4386    }
4387
4388    #[test]
4389    fn test_sorting_order_wrap_and_sort_substvars() {
4390        let order = WrapAndSortOrder;
4391        // Substvars should come after regular packages
4392        assert!(order.lt("python3", "${misc:Depends}"));
4393        assert!(!order.lt("${misc:Depends}", "python3"));
4394        // But wrap-and-sort doesn't ignore them
4395        assert!(!order.ignore("${misc:Depends}"));
4396    }
4397
4398    #[test]
4399    fn test_sorting_order_default_special_items() {
4400        let order = DefaultSortingOrder;
4401        // Special items should come after regular items
4402        assert!(order.lt("python3", "${misc:Depends}"));
4403        assert!(order.lt("aaa", "@cdbs@"));
4404        // And should be ignored
4405        assert!(order.ignore("${misc:Depends}"));
4406        assert!(order.ignore("@cdbs@"));
4407        assert!(!order.ignore("python3"));
4408    }
4409
4410    #[test]
4411    fn test_is_special_package_name() {
4412        assert!(is_special_package_name("${misc:Depends}"));
4413        assert!(is_special_package_name("${shlibs:Depends}"));
4414        assert!(is_special_package_name("@cdbs@"));
4415        assert!(!is_special_package_name("python3"));
4416        assert!(!is_special_package_name("debhelper"));
4417    }
4418
4419    #[test]
4420    fn test_add_dependency_with_explicit_position() {
4421        // Test that add_dependency works with explicit position and preserves whitespace
4422        let mut relations: Relations = "python3,  rustc".parse().unwrap();
4423        let entry = Entry::from(Relation::simple("debhelper"));
4424        relations.add_dependency(entry, Some(1));
4425        // Should preserve the 2-space pattern from the original
4426        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc");
4427    }
4428
4429    #[test]
4430    fn test_whitespace_detection_single_space() {
4431        let mut relations: Relations = "python3, rustc".parse().unwrap();
4432        let entry = Entry::from(Relation::simple("debhelper"));
4433        relations.add_dependency(entry, Some(1));
4434        assert_eq!(relations.to_string(), "python3, debhelper, rustc");
4435    }
4436
4437    #[test]
4438    fn test_whitespace_detection_multiple_spaces() {
4439        let mut relations: Relations = "python3,  rustc,  gcc".parse().unwrap();
4440        let entry = Entry::from(Relation::simple("debhelper"));
4441        relations.add_dependency(entry, Some(1));
4442        // Should detect and use the 2-space pattern
4443        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc,  gcc");
4444    }
4445
4446    #[test]
4447    fn test_whitespace_detection_mixed_patterns() {
4448        // When patterns differ, use the most common one
4449        let mut relations: Relations = "a, b, c,  d, e".parse().unwrap();
4450        let entry = Entry::from(Relation::simple("x"));
4451        relations.push(entry);
4452        // Three single-space (after a, b, d), one double-space (after c)
4453        // Should use single space as it's most common
4454        assert_eq!(relations.to_string(), "a, b, c,  d, e, x");
4455    }
4456
4457    #[test]
4458    fn test_whitespace_detection_newlines() {
4459        let mut relations: Relations = "python3,\n rustc".parse().unwrap();
4460        let entry = Entry::from(Relation::simple("debhelper"));
4461        relations.add_dependency(entry, Some(1));
4462        // Detects full pattern including newline
4463        assert_eq!(relations.to_string(), "python3,\n debhelper,\n rustc");
4464    }
4465
4466    #[test]
4467    fn test_append_with_newline_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, None);
4471        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4472    }
4473
4474    #[test]
4475    fn test_append_with_trailing_newline() {
4476        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4477        let entry = Entry::from(Relation::simple("blah"));
4478        relations.add_dependency(entry, None);
4479        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4480    }
4481
4482    #[test]
4483    fn test_append_with_4_space_indent() {
4484        let mut relations: Relations = "foo,\n    bar".parse().unwrap();
4485        let entry = Entry::from(Relation::simple("blah"));
4486        relations.add_dependency(entry, None);
4487        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4488    }
4489
4490    #[test]
4491    fn test_append_with_4_space_and_trailing_newline() {
4492        let mut relations: Relations = "foo,\n    bar\n".parse().unwrap();
4493        let entry = Entry::from(Relation::simple("blah"));
4494        relations.add_dependency(entry, None);
4495        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4496    }
4497
4498    #[test]
4499    fn test_odd_syntax_append_no_trailing() {
4500        let mut relations: Relations = "\n foo\n , bar".parse().unwrap();
4501        let entry = Entry::from(Relation::simple("blah"));
4502        relations.add_dependency(entry, None);
4503        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4504    }
4505
4506    #[test]
4507    fn test_odd_syntax_append_with_trailing() {
4508        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4509        let entry = Entry::from(Relation::simple("blah"));
4510        relations.add_dependency(entry, None);
4511        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4512    }
4513
4514    #[test]
4515    fn test_insert_at_1_no_trailing() {
4516        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4517        let entry = Entry::from(Relation::simple("blah"));
4518        relations.add_dependency(entry, Some(1));
4519        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4520    }
4521
4522    #[test]
4523    fn test_insert_at_1_with_trailing() {
4524        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4525        let entry = Entry::from(Relation::simple("blah"));
4526        relations.add_dependency(entry, Some(1));
4527        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4528    }
4529
4530    #[test]
4531    fn test_odd_syntax_insert_at_1() {
4532        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4533        let entry = Entry::from(Relation::simple("blah"));
4534        relations.add_dependency(entry, Some(1));
4535        assert_eq!(relations.to_string(), "\n foo\n , blah\n , bar");
4536    }
4537
4538    #[test]
4539    fn test_relations_preserves_exact_whitespace() {
4540        // Test that Relations preserves exact whitespace from input
4541        let input =
4542            "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], pkg-config";
4543
4544        let relations: Relations = input.parse().unwrap();
4545
4546        // The whitespace should be preserved in the syntax tree
4547        assert_eq!(
4548            relations.to_string(),
4549            input,
4550            "Relations should preserve exact whitespace from input"
4551        );
4552    }
4553
4554    #[test]
4555    fn test_remove_entry_preserves_indentation() {
4556        // Test that removing an entry preserves the indentation pattern
4557        let input = "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config";
4558
4559        let mut relations: Relations = input.parse().unwrap();
4560
4561        // Find and remove dh-systemd entry (index 2)
4562        let mut to_remove = Vec::new();
4563        for (idx, entry) in relations.entries().enumerate() {
4564            for relation in entry.relations() {
4565                if relation.name() == "dh-systemd" {
4566                    to_remove.push(idx);
4567                    break;
4568                }
4569            }
4570        }
4571
4572        for idx in to_remove.into_iter().rev() {
4573            relations.remove_entry(idx);
4574        }
4575
4576        let output = relations.to_string();
4577        println!("After removal: '{}'", output);
4578
4579        // The 4-space indentation should be preserved
4580        assert!(
4581            output.contains("\n    libsystemd-dev"),
4582            "Expected 4-space indentation to be preserved, but got:\n'{}'",
4583            output
4584        );
4585    }
4586
4587    #[test]
4588    fn test_relation_is_implied_by_same_package() {
4589        // Same package name with compatible version constraints
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, "1.5".parse().unwrap())),
4597        );
4598        assert!(inner.is_implied_by(&outer));
4599    }
4600
4601    #[test]
4602    fn test_relation_is_implied_by_different_package() {
4603        // Different package names should not imply
4604        let inner = Relation::new("pkg1", None);
4605        let outer = Relation::new("pkg2", None);
4606        assert!(!inner.is_implied_by(&outer));
4607    }
4608
4609    #[test]
4610    fn test_relation_is_implied_by_no_version() {
4611        // No version constraint is implied by any version
4612        let inner = Relation::new("pkg", None);
4613        let outer = Relation::new(
4614            "pkg",
4615            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4616        );
4617        assert!(inner.is_implied_by(&outer));
4618    }
4619
4620    #[test]
4621    fn test_relation_is_implied_by_identical() {
4622        // Identical relations imply each other
4623        let inner = Relation::new(
4624            "pkg",
4625            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4626        );
4627        let outer = Relation::new(
4628            "pkg",
4629            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4630        );
4631        assert!(inner.is_implied_by(&outer));
4632        assert!(outer.is_implied_by(&inner));
4633    }
4634
4635    #[test]
4636    fn test_relation_is_implied_by_greater_than_equal() {
4637        // pkg >= 1.0 is implied by pkg >= 2.0
4638        let inner = Relation::new(
4639            "pkg",
4640            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4641        );
4642        let outer = Relation::new(
4643            "pkg",
4644            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
4645        );
4646        assert!(inner.is_implied_by(&outer));
4647        assert!(!outer.is_implied_by(&inner));
4648
4649        // pkg >= 1.0 is implied by pkg = 2.0
4650        let outer = Relation::new(
4651            "pkg",
4652            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4653        );
4654        assert!(inner.is_implied_by(&outer));
4655
4656        // pkg >= 1.0 is implied by pkg >> 1.5
4657        let outer = Relation::new(
4658            "pkg",
4659            Some((VersionConstraint::GreaterThan, "1.5".parse().unwrap())),
4660        );
4661        assert!(inner.is_implied_by(&outer));
4662
4663        // pkg >= 3.0 is NOT implied by pkg >> 3.0 (>> 3.0 doesn't include 3.0 itself)
4664        let inner = Relation::new(
4665            "pkg",
4666            Some((VersionConstraint::GreaterThanEqual, "3.0".parse().unwrap())),
4667        );
4668        let outer = Relation::new(
4669            "pkg",
4670            Some((VersionConstraint::GreaterThan, "3.0".parse().unwrap())),
4671        );
4672        assert!(!inner.is_implied_by(&outer));
4673    }
4674
4675    #[test]
4676    fn test_relation_is_implied_by_less_than_equal() {
4677        // pkg <= 2.0 is implied by pkg <= 1.0
4678        let inner = Relation::new(
4679            "pkg",
4680            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4681        );
4682        let outer = Relation::new(
4683            "pkg",
4684            Some((VersionConstraint::LessThanEqual, "1.0".parse().unwrap())),
4685        );
4686        assert!(inner.is_implied_by(&outer));
4687        assert!(!outer.is_implied_by(&inner));
4688
4689        // pkg <= 2.0 is implied by pkg = 1.0
4690        let outer = Relation::new(
4691            "pkg",
4692            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4693        );
4694        assert!(inner.is_implied_by(&outer));
4695
4696        // pkg <= 2.0 is implied by pkg << 1.5
4697        let outer = Relation::new(
4698            "pkg",
4699            Some((VersionConstraint::LessThan, "1.5".parse().unwrap())),
4700        );
4701        assert!(inner.is_implied_by(&outer));
4702    }
4703
4704    #[test]
4705    fn test_relation_is_implied_by_equal() {
4706        // pkg = 1.0 is only implied by pkg = 1.0
4707        let inner = Relation::new(
4708            "pkg",
4709            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4710        );
4711        let outer = Relation::new(
4712            "pkg",
4713            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4714        );
4715        assert!(inner.is_implied_by(&outer));
4716
4717        // Not implied by different version
4718        let outer = Relation::new(
4719            "pkg",
4720            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4721        );
4722        assert!(!inner.is_implied_by(&outer));
4723
4724        // Not implied by >= constraint
4725        let outer = Relation::new(
4726            "pkg",
4727            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4728        );
4729        assert!(!inner.is_implied_by(&outer));
4730    }
4731
4732    #[test]
4733    fn test_relation_is_implied_by_greater_than() {
4734        // pkg >> 1.0 is implied by pkg >> 2.0
4735        let inner = Relation::new(
4736            "pkg",
4737            Some((VersionConstraint::GreaterThan, "1.0".parse().unwrap())),
4738        );
4739        let outer = Relation::new(
4740            "pkg",
4741            Some((VersionConstraint::GreaterThan, "2.0".parse().unwrap())),
4742        );
4743        assert!(inner.is_implied_by(&outer));
4744
4745        // pkg >> 1.0 is implied by pkg = 2.0
4746        let outer = Relation::new(
4747            "pkg",
4748            Some((VersionConstraint::Equal, "2.0".parse().unwrap())),
4749        );
4750        assert!(inner.is_implied_by(&outer));
4751
4752        // pkg >> 1.0 is implied by pkg >= 1.5 (strictly greater)
4753        let outer = Relation::new(
4754            "pkg",
4755            Some((VersionConstraint::GreaterThanEqual, "1.5".parse().unwrap())),
4756        );
4757        assert!(inner.is_implied_by(&outer));
4758
4759        // pkg >> 1.0 is NOT implied by pkg >= 1.0 (could be equal)
4760        let outer = Relation::new(
4761            "pkg",
4762            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4763        );
4764        assert!(!inner.is_implied_by(&outer));
4765    }
4766
4767    #[test]
4768    fn test_relation_is_implied_by_less_than() {
4769        // pkg << 2.0 is implied by pkg << 1.0
4770        let inner = Relation::new(
4771            "pkg",
4772            Some((VersionConstraint::LessThan, "2.0".parse().unwrap())),
4773        );
4774        let outer = Relation::new(
4775            "pkg",
4776            Some((VersionConstraint::LessThan, "1.0".parse().unwrap())),
4777        );
4778        assert!(inner.is_implied_by(&outer));
4779
4780        // pkg << 2.0 is implied by pkg = 1.0
4781        let outer = Relation::new(
4782            "pkg",
4783            Some((VersionConstraint::Equal, "1.0".parse().unwrap())),
4784        );
4785        assert!(inner.is_implied_by(&outer));
4786
4787        // pkg << 2.0 is implied by pkg <= 1.5 (strictly less)
4788        let outer = Relation::new(
4789            "pkg",
4790            Some((VersionConstraint::LessThanEqual, "1.5".parse().unwrap())),
4791        );
4792        assert!(inner.is_implied_by(&outer));
4793    }
4794
4795    #[test]
4796    fn test_relation_is_implied_by_incompatible_constraints() {
4797        // >= and <= are incompatible
4798        let inner = Relation::new(
4799            "pkg",
4800            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap())),
4801        );
4802        let outer = Relation::new(
4803            "pkg",
4804            Some((VersionConstraint::LessThanEqual, "2.0".parse().unwrap())),
4805        );
4806        assert!(!inner.is_implied_by(&outer));
4807        assert!(!outer.is_implied_by(&inner));
4808    }
4809
4810    #[test]
4811    fn test_entry_is_implied_by_identical() {
4812        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4813        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4814        assert!(inner.is_implied_by(&outer));
4815    }
4816
4817    #[test]
4818    fn test_entry_is_implied_by_or_group() {
4819        // "pkg >= 1.0" is implied by "pkg >= 1.5 | libc6"
4820        let inner: Entry = "pkg (>= 1.0)".parse().unwrap();
4821        let outer: Entry = "pkg (>= 1.5) | libc6".parse().unwrap();
4822        assert!(inner.is_implied_by(&outer));
4823    }
4824
4825    #[test]
4826    fn test_entry_is_implied_by_simple_or() {
4827        // "pkg1 | pkg2" is implied by "pkg1" (first alternative satisfies)
4828        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4829        let outer: Entry = "pkg1".parse().unwrap();
4830        assert!(inner.is_implied_by(&outer));
4831
4832        // Also implied by "pkg2"
4833        let outer: Entry = "pkg2".parse().unwrap();
4834        assert!(inner.is_implied_by(&outer));
4835    }
4836
4837    #[test]
4838    fn test_entry_is_implied_by_not_implied() {
4839        // "pkg >= 2.0" is NOT implied by "pkg >= 1.0"
4840        let inner: Entry = "pkg (>= 2.0)".parse().unwrap();
4841        let outer: Entry = "pkg (>= 1.0)".parse().unwrap();
4842        assert!(!inner.is_implied_by(&outer));
4843    }
4844
4845    #[test]
4846    fn test_entry_is_implied_by_different_packages() {
4847        let inner: Entry = "pkg1".parse().unwrap();
4848        let outer: Entry = "pkg2".parse().unwrap();
4849        assert!(!inner.is_implied_by(&outer));
4850    }
4851
4852    #[test]
4853    fn test_entry_is_implied_by_complex_or() {
4854        // "pkg1 | pkg2" is implied by "pkg1 | pkg2" (identical)
4855        let inner: Entry = "pkg1 | pkg2".parse().unwrap();
4856        let outer: Entry = "pkg1 | pkg2".parse().unwrap();
4857        assert!(inner.is_implied_by(&outer));
4858
4859        // "pkg1 | pkg2" is implied by "pkg1 | pkg2 | pkg3" (one matches)
4860        let outer: Entry = "pkg1 | pkg2 | pkg3".parse().unwrap();
4861        assert!(inner.is_implied_by(&outer));
4862    }
4863
4864    #[test]
4865    fn test_parse_version_with_epoch() {
4866        // Test parsing version strings with epoch (e.g., "1:2.3.2-2~")
4867        // The colon should be treated as part of the version, not as a delimiter
4868        let input = "amule-dbg (<< 1:2.3.2-2~)";
4869        let parsed: Relations = input.parse().unwrap();
4870        assert_eq!(parsed.to_string(), input);
4871        assert_eq!(parsed.entries().count(), 1);
4872        let entry = parsed.entries().next().unwrap();
4873        assert_eq!(entry.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4874        assert_eq!(entry.relations().count(), 1);
4875        let relation = entry.relations().next().unwrap();
4876        assert_eq!(relation.name(), "amule-dbg");
4877        assert_eq!(relation.to_string(), "amule-dbg (<< 1:2.3.2-2~)");
4878        assert_eq!(
4879            relation.version(),
4880            Some((VersionConstraint::LessThan, "1:2.3.2-2~".parse().unwrap()))
4881        );
4882    }
4883
4884    #[test]
4885    fn test_ensure_relation_add_new() {
4886        // Test adding a new relation that doesn't exist yet
4887        let mut relations: Relations = "python3".parse().unwrap();
4888        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4889        let added = relations.ensure_relation(new_entry);
4890        assert!(added);
4891        // debhelper is inserted in sorted position (alphabetically before python3)
4892        assert_eq!(relations.to_string(), "debhelper (>= 12), python3");
4893    }
4894
4895    #[test]
4896    fn test_ensure_relation_already_satisfied() {
4897        // Test that a relation is not added if it's already satisfied by a stronger constraint
4898        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
4899        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4900        let added = relations.ensure_relation(new_entry);
4901        assert!(!added);
4902        assert_eq!(relations.to_string(), "debhelper (>= 13)");
4903    }
4904
4905    #[test]
4906    fn test_ensure_relation_replace_weaker() {
4907        // Test that a weaker relation is replaced with a stronger one
4908        let mut relations: Relations = "debhelper (>= 11)".parse().unwrap();
4909        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4910        let added = relations.ensure_relation(new_entry);
4911        assert!(added);
4912        assert_eq!(relations.to_string(), "debhelper (>= 13)");
4913    }
4914
4915    #[test]
4916    fn test_ensure_relation_replace_multiple_weaker() {
4917        // Test that multiple weaker relations are replaced/removed when a stronger one is added
4918        let mut relations: Relations = "debhelper (>= 11), debhelper (>= 10), python3"
4919            .parse()
4920            .unwrap();
4921        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
4922        let added = relations.ensure_relation(new_entry);
4923        assert!(added);
4924        assert_eq!(relations.to_string(), "debhelper (>= 13), python3");
4925    }
4926
4927    #[test]
4928    fn test_ensure_relation_identical_entry() {
4929        // Test that an identical entry is not added again
4930        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
4931        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4932        let added = relations.ensure_relation(new_entry);
4933        assert!(!added);
4934        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4935    }
4936
4937    #[test]
4938    fn test_ensure_relation_no_version_constraint() {
4939        // Test that a relation without version constraint is added
4940        let mut relations: Relations = "python3".parse().unwrap();
4941        let new_entry: Entry = "debhelper".parse().unwrap();
4942        let added = relations.ensure_relation(new_entry);
4943        assert!(added);
4944        // debhelper is inserted in sorted position (alphabetically before python3)
4945        assert_eq!(relations.to_string(), "debhelper, python3");
4946    }
4947
4948    #[test]
4949    fn test_ensure_relation_strengthen_unversioned() {
4950        // Test that a versioned constraint replaces an unversioned one
4951        // An unversioned dependency is weaker than a versioned one
4952        let mut relations: Relations = "debhelper".parse().unwrap();
4953        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4954        let added = relations.ensure_relation(new_entry);
4955        assert!(added);
4956        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4957    }
4958
4959    #[test]
4960    fn test_ensure_relation_versioned_implies_unversioned() {
4961        // Test that an unversioned dependency is already satisfied by a versioned one
4962        // A versioned dependency is stronger and implies the unversioned one
4963        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
4964        let new_entry: Entry = "debhelper".parse().unwrap();
4965        let added = relations.ensure_relation(new_entry);
4966        assert!(!added);
4967        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4968    }
4969
4970    #[test]
4971    fn test_ensure_relation_preserves_whitespace() {
4972        // Test that whitespace is preserved when adding a new relation
4973        let mut relations: Relations = "python3,  rustc".parse().unwrap();
4974        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4975        let added = relations.ensure_relation(new_entry);
4976        assert!(added);
4977        // debhelper is inserted in sorted position (alphabetically before python3 and rustc)
4978        assert_eq!(relations.to_string(), "debhelper (>= 12),  python3,  rustc");
4979    }
4980
4981    #[test]
4982    fn test_ensure_relation_empty_relations() {
4983        // Test adding to empty relations
4984        let mut relations: Relations = Relations::new();
4985        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4986        let added = relations.ensure_relation(new_entry);
4987        assert!(added);
4988        assert_eq!(relations.to_string(), "debhelper (>= 12)");
4989    }
4990
4991    #[test]
4992    fn test_ensure_relation_alternative_dependencies() {
4993        // Test with alternative dependencies (|)
4994        let mut relations: Relations = "python3 | python3-minimal".parse().unwrap();
4995        let new_entry: Entry = "debhelper (>= 12)".parse().unwrap();
4996        let added = relations.ensure_relation(new_entry);
4997        assert!(added);
4998        // debhelper is inserted in sorted position (alphabetically before python3)
4999        assert_eq!(
5000            relations.to_string(),
5001            "debhelper (>= 12), python3 | python3-minimal"
5002        );
5003    }
5004
5005    #[test]
5006    fn test_ensure_relation_replace_in_middle() {
5007        // Test that replacing a weaker entry in the middle preserves order
5008        let mut relations: Relations = "python3, debhelper (>= 11), rustc".parse().unwrap();
5009        let new_entry: Entry = "debhelper (>= 13)".parse().unwrap();
5010        let added = relations.ensure_relation(new_entry);
5011        assert!(added);
5012        assert_eq!(relations.to_string(), "python3, debhelper (>= 13), rustc");
5013    }
5014
5015    #[test]
5016    fn test_ensure_relation_with_different_package() {
5017        // Test that adding a different package doesn't affect existing ones
5018        let mut relations: Relations = "python3, debhelper (>= 12)".parse().unwrap();
5019        let new_entry: Entry = "rustc".parse().unwrap();
5020        let added = relations.ensure_relation(new_entry);
5021        assert!(added);
5022        assert_eq!(relations.to_string(), "python3, debhelper (>= 12), rustc");
5023    }
5024
5025    #[test]
5026    fn test_parse_invalid_token_in_arch_list() {
5027        let input = "foo [>= bar]";
5028        let result: Result<Relations, _> = input.parse();
5029        assert!(
5030            result.is_err(),
5031            "Expected error for invalid token in architecture list"
5032        );
5033    }
5034
5035    #[test]
5036    fn test_parse_invalid_token_in_profile_list() {
5037        let input = "foo <[] baz>";
5038        let result: Result<Relations, _> = input.parse();
5039        assert!(
5040            result.is_err(),
5041            "Expected error for invalid token in profile list"
5042        );
5043    }
5044
5045    #[test]
5046    fn test_parse_relaxed_unterminated_arch_list() {
5047        let (relations, errors) = Relations::parse_relaxed("libc6 [", true);
5048        assert!(!errors.is_empty());
5049        assert_eq!(relations.to_string(), "libc6 [");
5050    }
5051
5052    #[test]
5053    fn test_parse_relaxed_partial_arch_name() {
5054        let (relations, errors) = Relations::parse_relaxed("libc6 [amd", true);
5055        assert!(!errors.is_empty());
5056        assert_eq!(relations.to_string(), "libc6 [amd");
5057    }
5058
5059    #[test]
5060    fn test_parse_relaxed_unterminated_profile_list() {
5061        let (relations, errors) = Relations::parse_relaxed("libc6 <cross", true);
5062        assert!(!errors.is_empty());
5063        assert_eq!(relations.to_string(), "libc6 <cross");
5064    }
5065
5066    #[test]
5067    fn test_parse_relaxed_unterminated_substvar() {
5068        let (relations, errors) = Relations::parse_relaxed("${shlibs:Depends", true);
5069        assert!(!errors.is_empty());
5070        assert_eq!(relations.to_string(), "${shlibs:Depends");
5071    }
5072
5073    #[test]
5074    fn test_parse_relaxed_empty_substvar() {
5075        let (relations, errors) = Relations::parse_relaxed("${", true);
5076        assert!(!errors.is_empty());
5077        assert_eq!(relations.to_string(), "${");
5078    }
5079
5080    #[test]
5081    fn test_parse_with_comments() {
5082        let input = "dh-python,\nlibsvn-dev,\n#               python-all-dbg (>= 2.6.6-3),\npython3-all-dev,\n#               python3-all-dbg,\npython3-docutils";
5083        let relations: Relations = input.parse().unwrap();
5084        let entries: Vec<_> = relations.entries().collect();
5085        assert_eq!(entries.len(), 4);
5086        assert_eq!(entries[0].to_string(), "dh-python");
5087        assert_eq!(entries[1].to_string(), "libsvn-dev");
5088        assert_eq!(entries[2].to_string(), "python3-all-dev");
5089        assert_eq!(entries[3].to_string(), "python3-docutils");
5090        // Round-trip preserves comments
5091        assert_eq!(relations.to_string(), input);
5092    }
5093
5094    #[test]
5095    fn test_remove_entry_with_adjacent_comment() {
5096        let input = "dh-python,\n#  commented-out,\npython3-all-dev";
5097        let mut relations: Relations = input.parse().unwrap();
5098        assert_eq!(relations.entries().count(), 2);
5099        relations.remove_entry(0);
5100        assert_eq!(relations.entries().count(), 1);
5101        assert_eq!(
5102            relations.entries().next().unwrap().to_string(),
5103            "python3-all-dev"
5104        );
5105    }
5106
5107    #[test]
5108    fn test_insert_entry_with_comments_present() {
5109        let input = "dh-python,\n#  commented-out,\npython3-all-dev";
5110        let mut relations: Relations = input.parse().unwrap();
5111        let new_entry: Entry = "libfoo-dev".parse().unwrap();
5112        relations.push(new_entry);
5113        // New entry should be appended
5114        let entries: Vec<_> = relations.entries().collect();
5115        assert_eq!(entries.len(), 3);
5116        assert_eq!(entries[2].to_string(), "libfoo-dev");
5117    }
5118
5119    #[test]
5120    fn test_drop_dependency_with_comments() {
5121        let input = "dh-python,\n#  commented-out,\npython3-all-dev,\nlibfoo-dev";
5122        let mut relations: Relations = input.parse().unwrap();
5123        assert!(relations.drop_dependency("python3-all-dev"));
5124        let entries: Vec<_> = relations.entries().collect();
5125        assert_eq!(entries.len(), 2);
5126        assert_eq!(entries[0].to_string(), "dh-python");
5127        assert_eq!(entries[1].to_string(), "libfoo-dev");
5128    }
5129
5130    #[test]
5131    fn test_relation_remove_first_with_malformed_tree() {
5132        // Relaxed parsing can produce ERROR nodes inside an entry.
5133        // Removing the first relation should not panic on unexpected tokens.
5134        let (relations, errors) = Relations::parse_relaxed("foo @ bar | baz", false);
5135        assert!(!errors.is_empty());
5136        let entry = relations.get_entry(0).unwrap();
5137        let mut relation = entry.get_relation(0).unwrap();
5138        // Should not panic
5139        relation.remove();
5140    }
5141}