debian_control/lossless/
relations.rs

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