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    /// Filter entries based on a predicate function.
1307    ///
1308    /// # Arguments
1309    /// * `keep` - A function that returns true for entries to keep
1310    ///
1311    /// # Example
1312    /// ```
1313    /// use debian_control::lossless::relations::Relations;
1314    ///
1315    /// let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1316    /// relations.filter_entries(|entry| {
1317    ///     entry.relations().any(|r| r.name().starts_with("python"))
1318    /// });
1319    /// assert_eq!(relations.to_string(), "python3");
1320    /// ```
1321    pub fn filter_entries<F>(&mut self, keep: F)
1322    where
1323        F: Fn(&Entry) -> bool,
1324    {
1325        let indices_to_remove: Vec<_> = self
1326            .entries()
1327            .enumerate()
1328            .filter_map(|(idx, entry)| if keep(&entry) { None } else { Some(idx) })
1329            .collect();
1330
1331        // Remove in reverse order to maintain correct indices
1332        for idx in indices_to_remove.into_iter().rev() {
1333            self.remove_entry(idx);
1334        }
1335    }
1336
1337    /// Check whether the relations are sorted according to a given sorting order.
1338    ///
1339    /// # Arguments
1340    /// * `sorting_order` - The sorting order to check against
1341    ///
1342    /// # Example
1343    /// ```
1344    /// use debian_control::lossless::relations::{Relations, WrapAndSortOrder};
1345    ///
1346    /// let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
1347    /// assert!(relations.is_sorted(&WrapAndSortOrder));
1348    ///
1349    /// let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
1350    /// assert!(!relations.is_sorted(&WrapAndSortOrder));
1351    /// ```
1352    pub fn is_sorted(&self, sorting_order: &impl SortingOrder) -> bool {
1353        let mut last_name: Option<String> = None;
1354        for entry in self.entries() {
1355            // Skip empty entries
1356            let mut relations = entry.relations();
1357            let Some(relation) = relations.next() else {
1358                continue;
1359            };
1360
1361            let name = relation.name();
1362
1363            // Skip items that should be ignored
1364            if sorting_order.ignore(&name) {
1365                continue;
1366            }
1367
1368            // Check if this breaks the sort order
1369            if let Some(ref last) = last_name {
1370                if sorting_order.lt(&name, last) {
1371                    return false;
1372                }
1373            }
1374
1375            last_name = Some(name);
1376        }
1377        true
1378    }
1379
1380    /// Find the position to insert an entry while maintaining sort order.
1381    ///
1382    /// This method detects the current sorting order and returns the appropriate
1383    /// insertion position. If there are fewer than 2 entries, it defaults to
1384    /// WrapAndSortOrder. If no sorting order is detected, it returns the end position.
1385    ///
1386    /// # Arguments
1387    /// * `entry` - The entry to insert
1388    ///
1389    /// # Returns
1390    /// The index where the entry should be inserted
1391    fn find_insert_position(&self, entry: &Entry) -> usize {
1392        // Get the package name from the first relation in the entry
1393        let Some(relation) = entry.relations().next() else {
1394            // Empty entry, just append at the end
1395            return self.len();
1396        };
1397        let package_name = relation.name();
1398
1399        // Count non-empty entries
1400        let count = self.entries().filter(|e| !e.is_empty()).count();
1401
1402        // If there are less than 2 items, default to WrapAndSortOrder
1403        let sorting_order: Box<dyn SortingOrder> = if count < 2 {
1404            Box::new(WrapAndSortOrder)
1405        } else {
1406            // Try to detect which sorting order is being used
1407            // Try WrapAndSortOrder first, then DefaultSortingOrder
1408            if self.is_sorted(&WrapAndSortOrder) {
1409                Box::new(WrapAndSortOrder)
1410            } else if self.is_sorted(&DefaultSortingOrder) {
1411                Box::new(DefaultSortingOrder)
1412            } else {
1413                // No sorting order detected, just append at the end
1414                return self.len();
1415            }
1416        };
1417
1418        // If adding a special item that should be ignored by this sort order, append at the end
1419        if sorting_order.ignore(&package_name) {
1420            return self.len();
1421        }
1422
1423        // Insert in sorted order among regular items
1424        let mut position = 0;
1425        for (idx, existing_entry) in self.entries().enumerate() {
1426            let mut existing_relations = existing_entry.relations();
1427            let Some(existing_relation) = existing_relations.next() else {
1428                // Empty entry, skip
1429                position += 1;
1430                continue;
1431            };
1432
1433            let existing_name = existing_relation.name();
1434
1435            // Skip special items when finding insertion position
1436            if sorting_order.ignore(&existing_name) {
1437                position += 1;
1438                continue;
1439            }
1440
1441            // Compare with regular items only
1442            if sorting_order.lt(&package_name, &existing_name) {
1443                return idx;
1444            }
1445            position += 1;
1446        }
1447
1448        position
1449    }
1450
1451    /// Drop a dependency from the relations by package name.
1452    ///
1453    /// # Arguments
1454    /// * `package` - The package name to remove
1455    ///
1456    /// # Returns
1457    /// `true` if the package was found and removed, `false` otherwise
1458    ///
1459    /// # Example
1460    /// ```
1461    /// use debian_control::lossless::relations::Relations;
1462    ///
1463    /// let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1464    /// assert!(relations.drop_dependency("debhelper"));
1465    /// assert_eq!(relations.to_string(), "python3, rustc");
1466    /// assert!(!relations.drop_dependency("nonexistent"));
1467    /// ```
1468    pub fn drop_dependency(&mut self, package: &str) -> bool {
1469        let indices_to_remove: Vec<_> = self
1470            .entries()
1471            .enumerate()
1472            .filter_map(|(idx, entry)| {
1473                let relations: Vec<_> = entry.relations().collect();
1474                let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1475                if names == vec![package] {
1476                    Some(idx)
1477                } else {
1478                    None
1479                }
1480            })
1481            .collect();
1482
1483        let found = !indices_to_remove.is_empty();
1484
1485        // Remove in reverse order to maintain correct indices
1486        for idx in indices_to_remove.into_iter().rev() {
1487            self.remove_entry(idx);
1488        }
1489
1490        found
1491    }
1492
1493    /// Add a dependency at a specific position or auto-detect the position.
1494    ///
1495    /// If `position` is `None`, the position is automatically determined based
1496    /// on the detected sorting order. If a sorting order is detected, the entry
1497    /// is inserted in the appropriate position to maintain that order. Otherwise,
1498    /// it is appended at the end.
1499    ///
1500    /// # Arguments
1501    /// * `entry` - The entry to add
1502    /// * `position` - Optional position to insert at
1503    ///
1504    /// # Example
1505    /// ```
1506    /// use debian_control::lossless::relations::{Relations, Relation, Entry};
1507    ///
1508    /// let mut relations: Relations = "python3, rustc".parse().unwrap();
1509    /// let entry = Entry::from(Relation::simple("debhelper"));
1510    /// relations.add_dependency(entry, None);
1511    /// // debhelper is inserted in sorted order (if order is detected)
1512    /// ```
1513    pub fn add_dependency(&mut self, entry: Entry, position: Option<usize>) {
1514        let pos = position.unwrap_or_else(|| self.find_insert_position(&entry));
1515        self.insert(pos, entry);
1516    }
1517
1518    /// Get the entry containing a specific package.
1519    ///
1520    /// This returns the first entry that contains exactly one relation with the
1521    /// specified package name (no alternatives).
1522    ///
1523    /// # Arguments
1524    /// * `package` - The package name to search for
1525    ///
1526    /// # Returns
1527    /// A tuple of (index, Entry) if found
1528    ///
1529    /// # Errors
1530    /// Returns `Err` with a message if the package is found in a complex rule (with alternatives)
1531    ///
1532    /// # Example
1533    /// ```
1534    /// use debian_control::lossless::relations::Relations;
1535    ///
1536    /// let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
1537    /// let (idx, entry) = relations.get_relation("debhelper").unwrap();
1538    /// assert_eq!(idx, 1);
1539    /// assert_eq!(entry.to_string(), "debhelper (>= 12)");
1540    /// ```
1541    pub fn get_relation(&self, package: &str) -> Result<(usize, Entry), String> {
1542        for (idx, entry) in self.entries().enumerate() {
1543            let relations: Vec<_> = entry.relations().collect();
1544            let names: Vec<_> = relations.iter().map(|r| r.name()).collect();
1545
1546            if names.len() > 1 && names.contains(&package.to_string()) {
1547                return Err(format!("Complex rule for {}, aborting", package));
1548            }
1549
1550            if names.len() == 1 && names[0] == package {
1551                return Ok((idx, entry));
1552            }
1553        }
1554        Err(format!("Package {} not found", package))
1555    }
1556
1557    /// Iterate over all entries containing a specific package.
1558    ///
1559    /// # Arguments
1560    /// * `package` - The package name to search for
1561    ///
1562    /// # Returns
1563    /// An iterator over tuples of (index, Entry)
1564    ///
1565    /// # Example
1566    /// ```
1567    /// use debian_control::lossless::relations::Relations;
1568    ///
1569    /// let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
1570    /// let entries: Vec<_> = relations.iter_relations_for("python3").collect();
1571    /// assert_eq!(entries.len(), 1);
1572    /// ```
1573    pub fn iter_relations_for(&self, package: &str) -> impl Iterator<Item = (usize, Entry)> + '_ {
1574        let package = package.to_string();
1575        self.entries().enumerate().filter(move |(_, entry)| {
1576            let names: Vec<_> = entry.relations().map(|r| r.name()).collect();
1577            names.contains(&package)
1578        })
1579    }
1580
1581    /// Check whether a package exists in the relations.
1582    ///
1583    /// # Arguments
1584    /// * `package` - The package name to search for
1585    ///
1586    /// # Returns
1587    /// `true` if the package is found, `false` otherwise
1588    ///
1589    /// # Example
1590    /// ```
1591    /// use debian_control::lossless::relations::Relations;
1592    ///
1593    /// let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
1594    /// assert!(relations.has_relation("debhelper"));
1595    /// assert!(!relations.has_relation("nonexistent"));
1596    /// ```
1597    pub fn has_relation(&self, package: &str) -> bool {
1598        self.entries()
1599            .any(|entry| entry.relations().any(|r| r.name() == package))
1600    }
1601}
1602
1603impl From<Vec<Entry>> for Relations {
1604    fn from(entries: Vec<Entry>) -> Self {
1605        let mut builder = GreenNodeBuilder::new();
1606        builder.start_node(ROOT.into());
1607        for (i, entry) in entries.into_iter().enumerate() {
1608            if i > 0 {
1609                builder.token(COMMA.into(), ",");
1610                builder.token(WHITESPACE.into(), " ");
1611            }
1612            inject(&mut builder, entry.0);
1613        }
1614        builder.finish_node();
1615        Relations(SyntaxNode::new_root_mut(builder.finish()))
1616    }
1617}
1618
1619impl From<Entry> for Relations {
1620    fn from(entry: Entry) -> Self {
1621        Self::from(vec![entry])
1622    }
1623}
1624
1625impl Default for Entry {
1626    fn default() -> Self {
1627        Self::new()
1628    }
1629}
1630
1631impl PartialOrd for Entry {
1632    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1633        let mut rels_a = self.relations();
1634        let mut rels_b = other.relations();
1635        while let (Some(a), Some(b)) = (rels_a.next(), rels_b.next()) {
1636            match a.cmp(&b) {
1637                std::cmp::Ordering::Equal => continue,
1638                x => return Some(x),
1639            }
1640        }
1641
1642        if rels_a.next().is_some() {
1643            return Some(std::cmp::Ordering::Greater);
1644        }
1645
1646        if rels_b.next().is_some() {
1647            return Some(std::cmp::Ordering::Less);
1648        }
1649
1650        Some(std::cmp::Ordering::Equal)
1651    }
1652}
1653
1654impl Eq for Entry {}
1655
1656impl Ord for Entry {
1657    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1658        self.partial_cmp(other).unwrap()
1659    }
1660}
1661
1662impl Entry {
1663    /// Create a new entry
1664    pub fn new() -> Self {
1665        let mut builder = GreenNodeBuilder::new();
1666        builder.start_node(SyntaxKind::ENTRY.into());
1667        builder.finish_node();
1668        Entry(SyntaxNode::new_root_mut(builder.finish()))
1669    }
1670
1671    /// Replace the relation at the given index
1672    pub fn replace(&mut self, idx: usize, relation: Relation) {
1673        let current_relation = self.get_relation(idx).unwrap();
1674
1675        let old_root = current_relation.0;
1676        let new_root = relation.0;
1677        // Preserve white the current relation has
1678        let mut prev = new_root.first_child_or_token();
1679        let mut new_head_len = 0;
1680        // First, strip off any whitespace from the new relation
1681        while let Some(p) = prev {
1682            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1683                new_head_len += 1;
1684                prev = p.next_sibling_or_token();
1685            } else {
1686                break;
1687            }
1688        }
1689        let mut new_tail_len = 0;
1690        let mut next = new_root.last_child_or_token();
1691        while let Some(n) = next {
1692            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1693                new_tail_len += 1;
1694                next = n.prev_sibling_or_token();
1695            } else {
1696                break;
1697            }
1698        }
1699        // Then, inherit the whitespace from the old relation
1700        let mut prev = old_root.first_child_or_token();
1701        let mut old_head = vec![];
1702        while let Some(p) = prev {
1703            if p.kind() == WHITESPACE || p.kind() == NEWLINE {
1704                old_head.push(p.clone());
1705                prev = p.next_sibling_or_token();
1706            } else {
1707                break;
1708            }
1709        }
1710        let mut old_tail = vec![];
1711        let mut next = old_root.last_child_or_token();
1712        while let Some(n) = next {
1713            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1714                old_tail.push(n.clone());
1715                next = n.prev_sibling_or_token();
1716            } else {
1717                break;
1718            }
1719        }
1720        new_root.splice_children(0..new_head_len, old_head);
1721        let tail_pos = new_root.children_with_tokens().count() - new_tail_len;
1722        new_root.splice_children(
1723            tail_pos - new_tail_len..tail_pos,
1724            old_tail.into_iter().rev(),
1725        );
1726        let index = old_root.index();
1727        self.0
1728            .splice_children(index..index + 1, vec![new_root.into()]);
1729    }
1730
1731    /// Wrap and sort the relations in this entry
1732    #[must_use]
1733    pub fn wrap_and_sort(&self) -> Self {
1734        let mut relations = self
1735            .relations()
1736            .map(|r| r.wrap_and_sort())
1737            .collect::<Vec<_>>();
1738        // TODO: preserve comments
1739        relations.sort();
1740        Self::from(relations)
1741    }
1742
1743    /// Iterate over the relations in this entry
1744    pub fn relations(&self) -> impl Iterator<Item = Relation> + '_ {
1745        self.0.children().filter_map(Relation::cast)
1746    }
1747
1748    /// Iterate over the relations in this entry
1749    pub fn iter(&self) -> impl Iterator<Item = Relation> + '_ {
1750        self.relations()
1751    }
1752
1753    /// Get the relation at the given index
1754    pub fn get_relation(&self, idx: usize) -> Option<Relation> {
1755        self.relations().nth(idx)
1756    }
1757
1758    /// Remove the relation at the given index
1759    ///
1760    /// # Arguments
1761    /// * `idx` - The index of the relation to remove
1762    ///
1763    /// # Example
1764    /// ```
1765    /// use debian_control::lossless::relations::{Relation,Entry};
1766    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-requests".parse().unwrap();
1767    /// entry.remove_relation(1);
1768    /// assert_eq!(entry.to_string(), "python3-dulwich (>= 0.19.0)");
1769    /// ```
1770    pub fn remove_relation(&self, idx: usize) -> Relation {
1771        let mut relation = self.get_relation(idx).unwrap();
1772        relation.remove();
1773        relation
1774    }
1775
1776    /// Check if this entry is satisfied by the given package versions.
1777    ///
1778    /// # Arguments
1779    /// * `package_version` - A function that returns the version of a package.
1780    ///
1781    /// # Example
1782    /// ```
1783    /// use debian_control::lossless::relations::{Relation,Entry};
1784    /// let entry = Entry::from(vec!["samba (>= 2.0)".parse::<Relation>().unwrap()]);
1785    /// assert!(entry.satisfied_by(|name: &str| -> Option<debversion::Version> {
1786    ///    match name {
1787    ///    "samba" => Some("2.0".parse().unwrap()),
1788    ///    _ => None
1789    /// }}));
1790    /// ```
1791    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
1792        self.relations().any(|r| {
1793            let actual = package_version.lookup_version(r.name().as_str());
1794            if let Some((vc, version)) = r.version() {
1795                if let Some(actual) = actual {
1796                    match vc {
1797                        VersionConstraint::GreaterThanEqual => *actual >= version,
1798                        VersionConstraint::LessThanEqual => *actual <= version,
1799                        VersionConstraint::Equal => *actual == version,
1800                        VersionConstraint::GreaterThan => *actual > version,
1801                        VersionConstraint::LessThan => *actual < version,
1802                    }
1803                } else {
1804                    false
1805                }
1806            } else {
1807                actual.is_some()
1808            }
1809        })
1810    }
1811
1812    /// Remove this entry
1813    ///
1814    /// # Example
1815    /// ```
1816    /// use debian_control::lossless::relations::{Relations,Entry};
1817    /// let mut relations: Relations = r"python3-dulwich (>= 0.19.0), python3-urllib3 (<< 1.26.0)".parse().unwrap();
1818    /// let mut entry = relations.get_entry(0).unwrap();
1819    /// entry.remove();
1820    /// assert_eq!(relations.to_string(), "python3-urllib3 (<< 1.26.0)");
1821    /// ```
1822    pub fn remove(&mut self) {
1823        let mut removed_comma = false;
1824        let is_first = !self
1825            .0
1826            .siblings(Direction::Prev)
1827            .skip(1)
1828            .any(|n| n.kind() == ENTRY);
1829        while let Some(n) = self.0.next_sibling_or_token() {
1830            if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1831                n.detach();
1832            } else if n.kind() == COMMA {
1833                n.detach();
1834                removed_comma = true;
1835                break;
1836            } else {
1837                panic!("Unexpected node: {:?}", n);
1838            }
1839        }
1840        if !is_first {
1841            while let Some(n) = self.0.prev_sibling_or_token() {
1842                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1843                    n.detach();
1844                } else if !removed_comma && n.kind() == COMMA {
1845                    n.detach();
1846                    break;
1847                } else {
1848                    break;
1849                }
1850            }
1851        } else {
1852            while let Some(n) = self.0.next_sibling_or_token() {
1853                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
1854                    n.detach();
1855                } else {
1856                    break;
1857                }
1858            }
1859        }
1860        self.0.detach();
1861    }
1862
1863    /// Check if this entry is empty
1864    pub fn is_empty(&self) -> bool {
1865        self.relations().count() == 0
1866    }
1867
1868    /// Get the number of relations in this entry
1869    pub fn len(&self) -> usize {
1870        self.relations().count()
1871    }
1872
1873    /// Push a new relation to the entry
1874    ///
1875    /// # Arguments
1876    /// * `relation` - The relation to push
1877    ///
1878    /// # Example
1879    /// ```
1880    /// use debian_control::lossless::relations::{Relation,Entry};
1881    /// let mut entry: Entry = "samba (>= 2.0)".parse().unwrap();
1882    /// entry.push("python3-requests".parse().unwrap());
1883    /// assert_eq!(entry.to_string(), "samba (>= 2.0) | python3-requests");
1884    /// ```
1885    pub fn push(&mut self, relation: Relation) {
1886        let is_empty = !self
1887            .0
1888            .children_with_tokens()
1889            .any(|n| n.kind() == PIPE || n.kind() == RELATION);
1890
1891        let (position, new_children) = if let Some(current_relation) = self.relations().last() {
1892            let to_insert: Vec<NodeOrToken<GreenNode, GreenToken>> = if is_empty {
1893                vec![relation.0.green().into()]
1894            } else {
1895                vec![
1896                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
1897                    NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
1898                    NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
1899                    relation.0.green().into(),
1900                ]
1901            };
1902
1903            (current_relation.0.index() + 1, to_insert)
1904        } else {
1905            let child_count = self.0.children_with_tokens().count();
1906            (
1907                child_count,
1908                if is_empty {
1909                    vec![relation.0.green().into()]
1910                } else {
1911                    vec![
1912                        NodeOrToken::Token(GreenToken::new(PIPE.into(), "|")),
1913                        NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")),
1914                        relation.0.green().into(),
1915                    ]
1916                },
1917            )
1918        };
1919
1920        let new_root = SyntaxNode::new_root_mut(
1921            self.0.replace_with(
1922                self.0
1923                    .green()
1924                    .splice_children(position..position, new_children),
1925            ),
1926        );
1927
1928        if let Some(parent) = self.0.parent() {
1929            parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
1930            self.0 = parent
1931                .children_with_tokens()
1932                .nth(self.0.index())
1933                .unwrap()
1934                .clone()
1935                .into_node()
1936                .unwrap();
1937        } else {
1938            self.0 = new_root;
1939        }
1940    }
1941}
1942
1943fn inject(builder: &mut GreenNodeBuilder, node: SyntaxNode) {
1944    builder.start_node(node.kind().into());
1945    for child in node.children_with_tokens() {
1946        match child {
1947            rowan::NodeOrToken::Node(child) => {
1948                inject(builder, child);
1949            }
1950            rowan::NodeOrToken::Token(token) => {
1951                builder.token(token.kind().into(), token.text());
1952            }
1953        }
1954    }
1955    builder.finish_node();
1956}
1957
1958impl From<Vec<Relation>> for Entry {
1959    fn from(relations: Vec<Relation>) -> Self {
1960        let mut builder = GreenNodeBuilder::new();
1961        builder.start_node(SyntaxKind::ENTRY.into());
1962        for (i, relation) in relations.into_iter().enumerate() {
1963            if i > 0 {
1964                builder.token(WHITESPACE.into(), " ");
1965                builder.token(COMMA.into(), "|");
1966                builder.token(WHITESPACE.into(), " ");
1967            }
1968            inject(&mut builder, relation.0);
1969        }
1970        builder.finish_node();
1971        Entry(SyntaxNode::new_root_mut(builder.finish()))
1972    }
1973}
1974
1975impl From<Relation> for Entry {
1976    fn from(relation: Relation) -> Self {
1977        Self::from(vec![relation])
1978    }
1979}
1980
1981impl Relation {
1982    /// Create a new relation
1983    ///
1984    /// # Arguments
1985    /// * `name` - The name of the package
1986    /// * `version_constraint` - The version constraint and version to use
1987    ///
1988    /// # Example
1989    /// ```
1990    /// use debian_control::lossless::relations::{Relation};
1991    /// use debian_control::relations::VersionConstraint;
1992    /// let relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
1993    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
1994    /// ```
1995    pub fn new(name: &str, version_constraint: Option<(VersionConstraint, Version)>) -> Self {
1996        let mut builder = GreenNodeBuilder::new();
1997        builder.start_node(SyntaxKind::RELATION.into());
1998        builder.token(IDENT.into(), name);
1999        if let Some((vc, version)) = version_constraint {
2000            builder.token(WHITESPACE.into(), " ");
2001            builder.start_node(SyntaxKind::VERSION.into());
2002            builder.token(L_PARENS.into(), "(");
2003            builder.start_node(SyntaxKind::CONSTRAINT.into());
2004            for c in vc.to_string().chars() {
2005                builder.token(
2006                    match c {
2007                        '>' => R_ANGLE.into(),
2008                        '<' => L_ANGLE.into(),
2009                        '=' => EQUAL.into(),
2010                        _ => unreachable!(),
2011                    },
2012                    c.to_string().as_str(),
2013                );
2014            }
2015            builder.finish_node();
2016
2017            builder.token(WHITESPACE.into(), " ");
2018
2019            builder.token(IDENT.into(), version.to_string().as_str());
2020
2021            builder.token(R_PARENS.into(), ")");
2022
2023            builder.finish_node();
2024        }
2025
2026        builder.finish_node();
2027        Relation(SyntaxNode::new_root_mut(builder.finish()))
2028    }
2029
2030    /// Wrap and sort this relation
2031    ///
2032    /// # Example
2033    /// ```
2034    /// use debian_control::lossless::relations::Relation;
2035    /// let relation = "  samba  (  >= 2.0) ".parse::<Relation>().unwrap();
2036    /// assert_eq!(relation.wrap_and_sort().to_string(), "samba (>= 2.0)");
2037    /// ```
2038    #[must_use]
2039    pub fn wrap_and_sort(&self) -> Self {
2040        let mut builder = GreenNodeBuilder::new();
2041        builder.start_node(SyntaxKind::RELATION.into());
2042        builder.token(IDENT.into(), self.name().as_str());
2043        if let Some(archqual) = self.archqual() {
2044            builder.token(COLON.into(), ":");
2045            builder.token(IDENT.into(), archqual.as_str());
2046        }
2047        if let Some((vc, version)) = self.version() {
2048            builder.token(WHITESPACE.into(), " ");
2049            builder.start_node(SyntaxKind::VERSION.into());
2050            builder.token(L_PARENS.into(), "(");
2051            builder.start_node(SyntaxKind::CONSTRAINT.into());
2052            builder.token(
2053                match vc {
2054                    VersionConstraint::GreaterThanEqual => R_ANGLE.into(),
2055                    VersionConstraint::LessThanEqual => L_ANGLE.into(),
2056                    VersionConstraint::Equal => EQUAL.into(),
2057                    VersionConstraint::GreaterThan => R_ANGLE.into(),
2058                    VersionConstraint::LessThan => L_ANGLE.into(),
2059                },
2060                vc.to_string().as_str(),
2061            );
2062            builder.finish_node();
2063            builder.token(WHITESPACE.into(), " ");
2064            builder.token(IDENT.into(), version.to_string().as_str());
2065            builder.token(R_PARENS.into(), ")");
2066            builder.finish_node();
2067        }
2068        if let Some(architectures) = self.architectures() {
2069            builder.token(WHITESPACE.into(), " ");
2070            builder.start_node(ARCHITECTURES.into());
2071            builder.token(L_BRACKET.into(), "[");
2072            for (i, arch) in architectures.enumerate() {
2073                if i > 0 {
2074                    builder.token(WHITESPACE.into(), " ");
2075                }
2076                builder.token(IDENT.into(), arch.as_str());
2077            }
2078            builder.token(R_BRACKET.into(), "]");
2079            builder.finish_node();
2080        }
2081        for profiles in self.profiles() {
2082            builder.token(WHITESPACE.into(), " ");
2083            builder.start_node(PROFILES.into());
2084            builder.token(L_ANGLE.into(), "<");
2085            for (i, profile) in profiles.into_iter().enumerate() {
2086                if i > 0 {
2087                    builder.token(WHITESPACE.into(), " ");
2088                }
2089                match profile {
2090                    BuildProfile::Disabled(name) => {
2091                        builder.token(NOT.into(), "!");
2092                        builder.token(IDENT.into(), name.as_str());
2093                    }
2094                    BuildProfile::Enabled(name) => {
2095                        builder.token(IDENT.into(), name.as_str());
2096                    }
2097                }
2098            }
2099            builder.token(R_ANGLE.into(), ">");
2100            builder.finish_node();
2101        }
2102        builder.finish_node();
2103        Relation(SyntaxNode::new_root_mut(builder.finish()))
2104    }
2105
2106    /// Create a new simple relation, without any version constraints.
2107    ///
2108    /// # Example
2109    /// ```
2110    /// use debian_control::lossless::relations::Relation;
2111    /// let relation = Relation::simple("samba");
2112    /// assert_eq!(relation.to_string(), "samba");
2113    /// ```
2114    pub fn simple(name: &str) -> Self {
2115        Self::new(name, None)
2116    }
2117
2118    /// Remove the version constraint from the relation.
2119    ///
2120    /// # Example
2121    /// ```
2122    /// use debian_control::lossless::relations::{Relation};
2123    /// use debian_control::relations::VersionConstraint;
2124    /// let mut relation = Relation::new("samba", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2125    /// relation.drop_constraint();
2126    /// assert_eq!(relation.to_string(), "samba");
2127    /// ```
2128    pub fn drop_constraint(&mut self) -> bool {
2129        let version_token = self.0.children().find(|n| n.kind() == VERSION);
2130        if let Some(version_token) = version_token {
2131            // Remove any whitespace before the version token
2132            while let Some(prev) = version_token.prev_sibling_or_token() {
2133                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2134                    prev.detach();
2135                } else {
2136                    break;
2137                }
2138            }
2139            version_token.detach();
2140            return true;
2141        }
2142
2143        false
2144    }
2145
2146    /// Return the name of the package in the relation.
2147    ///
2148    /// # Example
2149    /// ```
2150    /// use debian_control::lossless::relations::Relation;
2151    /// let relation = Relation::simple("samba");
2152    /// assert_eq!(relation.name(), "samba");
2153    /// ```
2154    pub fn name(&self) -> String {
2155        self.0
2156            .children_with_tokens()
2157            .find_map(|it| match it {
2158                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2159                _ => None,
2160            })
2161            .unwrap()
2162            .text()
2163            .to_string()
2164    }
2165
2166    /// Return the archqual
2167    ///
2168    /// # Example
2169    /// ```
2170    /// use debian_control::lossless::relations::Relation;
2171    /// let relation: Relation = "samba:any".parse().unwrap();
2172    /// assert_eq!(relation.archqual(), Some("any".to_string()));
2173    /// ```
2174    pub fn archqual(&self) -> Option<String> {
2175        let archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2176        let node = if let Some(archqual) = archqual {
2177            archqual.children_with_tokens().find_map(|it| match it {
2178                SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2179                _ => None,
2180            })
2181        } else {
2182            None
2183        };
2184        node.map(|n| n.text().to_string())
2185    }
2186
2187    /// Set the architecture qualifier for this relation.
2188    ///
2189    /// # Example
2190    /// ```
2191    /// use debian_control::lossless::relations::Relation;
2192    /// let mut relation = Relation::simple("samba");
2193    /// relation.set_archqual("any");
2194    /// assert_eq!(relation.to_string(), "samba:any");
2195    /// ```
2196    pub fn set_archqual(&mut self, archqual: &str) {
2197        let mut builder = GreenNodeBuilder::new();
2198        builder.start_node(ARCHQUAL.into());
2199        builder.token(COLON.into(), ":");
2200        builder.token(IDENT.into(), archqual);
2201        builder.finish_node();
2202
2203        let node_archqual = self.0.children().find(|n| n.kind() == ARCHQUAL);
2204        if let Some(node_archqual) = node_archqual {
2205            self.0.splice_children(
2206                node_archqual.index()..node_archqual.index() + 1,
2207                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2208            );
2209        } else {
2210            let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2211            let idx = if let Some(name_node) = name_node {
2212                name_node.index() + 1
2213            } else {
2214                0
2215            };
2216            self.0.splice_children(
2217                idx..idx,
2218                vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2219            );
2220        }
2221    }
2222
2223    /// Return the version constraint and the version it is constrained to.
2224    pub fn version(&self) -> Option<(VersionConstraint, Version)> {
2225        let vc = self.0.children().find(|n| n.kind() == VERSION);
2226        let vc = vc.as_ref()?;
2227        let constraint = vc.children().find(|n| n.kind() == CONSTRAINT);
2228
2229        let version = vc.children_with_tokens().find_map(|it| match it {
2230            SyntaxElement::Token(token) if token.kind() == IDENT => Some(token),
2231            _ => None,
2232        });
2233
2234        if let (Some(constraint), Some(version)) = (constraint, version) {
2235            let vc: VersionConstraint = constraint.to_string().parse().unwrap();
2236            Some((vc, (version.text().to_string()).parse().unwrap()))
2237        } else {
2238            None
2239        }
2240    }
2241
2242    /// Set the version constraint for this relation
2243    ///
2244    /// # Example
2245    /// ```
2246    /// use debian_control::lossless::relations::{Relation};
2247    /// use debian_control::relations::VersionConstraint;
2248    /// let mut relation = Relation::simple("samba");
2249    /// relation.set_version(Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())));
2250    /// assert_eq!(relation.to_string(), "samba (>= 2.0)");
2251    /// ```
2252    pub fn set_version(&mut self, version_constraint: Option<(VersionConstraint, Version)>) {
2253        let current_version = self.0.children().find(|n| n.kind() == VERSION);
2254        if let Some((vc, version)) = version_constraint {
2255            let mut builder = GreenNodeBuilder::new();
2256            builder.start_node(VERSION.into());
2257            builder.token(L_PARENS.into(), "(");
2258            builder.start_node(CONSTRAINT.into());
2259            match vc {
2260                VersionConstraint::GreaterThanEqual => {
2261                    builder.token(R_ANGLE.into(), ">");
2262                    builder.token(EQUAL.into(), "=");
2263                }
2264                VersionConstraint::LessThanEqual => {
2265                    builder.token(L_ANGLE.into(), "<");
2266                    builder.token(EQUAL.into(), "=");
2267                }
2268                VersionConstraint::Equal => {
2269                    builder.token(EQUAL.into(), "=");
2270                }
2271                VersionConstraint::GreaterThan => {
2272                    builder.token(R_ANGLE.into(), ">");
2273                }
2274                VersionConstraint::LessThan => {
2275                    builder.token(L_ANGLE.into(), "<");
2276                }
2277            }
2278            builder.finish_node(); // CONSTRAINT
2279            builder.token(WHITESPACE.into(), " ");
2280            builder.token(IDENT.into(), version.to_string().as_str());
2281            builder.token(R_PARENS.into(), ")");
2282            builder.finish_node(); // VERSION
2283
2284            if let Some(current_version) = current_version {
2285                self.0.splice_children(
2286                    current_version.index()..current_version.index() + 1,
2287                    vec![SyntaxNode::new_root_mut(builder.finish()).into()],
2288                );
2289            } else {
2290                let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT);
2291                let idx = if let Some(name_node) = name_node {
2292                    name_node.index() + 1
2293                } else {
2294                    0
2295                };
2296                let new_children = vec![
2297                    GreenToken::new(WHITESPACE.into(), " ").into(),
2298                    builder.finish().into(),
2299                ];
2300                let new_root = SyntaxNode::new_root_mut(
2301                    self.0.green().splice_children(idx..idx, new_children),
2302                );
2303                if let Some(parent) = self.0.parent() {
2304                    parent
2305                        .splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2306                    self.0 = parent
2307                        .children_with_tokens()
2308                        .nth(self.0.index())
2309                        .unwrap()
2310                        .clone()
2311                        .into_node()
2312                        .unwrap();
2313                } else {
2314                    self.0 = new_root;
2315                }
2316            }
2317        } else if let Some(current_version) = current_version {
2318            // Remove any whitespace before the version token
2319            while let Some(prev) = current_version.prev_sibling_or_token() {
2320                if prev.kind() == WHITESPACE || prev.kind() == NEWLINE {
2321                    prev.detach();
2322                } else {
2323                    break;
2324                }
2325            }
2326            current_version.detach();
2327        }
2328    }
2329
2330    /// Return an iterator over the architectures for this relation
2331    ///
2332    /// # Example
2333    /// ```
2334    /// use debian_control::lossless::relations::Relation;
2335    /// let relation: Relation = "samba [amd64]".parse().unwrap();
2336    /// assert_eq!(relation.architectures().unwrap().collect::<Vec<_>>(), vec!["amd64".to_string()]);
2337    /// ```
2338    pub fn architectures(&self) -> Option<impl Iterator<Item = String> + '_> {
2339        let architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES)?;
2340
2341        Some(architectures.children_with_tokens().filter_map(|node| {
2342            let token = node.as_token()?;
2343            if token.kind() == IDENT {
2344                Some(token.text().to_string())
2345            } else {
2346                None
2347            }
2348        }))
2349    }
2350
2351    /// Returns an iterator over the build profiles for this relation
2352    ///
2353    /// # Example
2354    /// ```
2355    /// use debian_control::lossless::relations::{Relation};
2356    /// use debian_control::relations::{BuildProfile};
2357    /// let relation: Relation = "samba <!nocheck>".parse().unwrap();
2358    /// assert_eq!(relation.profiles().collect::<Vec<_>>(), vec![vec![BuildProfile::Disabled("nocheck".to_string())]]);
2359    /// ```
2360    pub fn profiles(&self) -> impl Iterator<Item = Vec<BuildProfile>> + '_ {
2361        let profiles = self.0.children().filter(|n| n.kind() == PROFILES);
2362
2363        profiles.map(|profile| {
2364            // iterate over nodes separated by whitespace tokens
2365            let mut ret = vec![];
2366            let mut current = vec![];
2367            for token in profile.children_with_tokens() {
2368                match token.kind() {
2369                    WHITESPACE | NEWLINE => {
2370                        if !current.is_empty() {
2371                            ret.push(current.join("").parse::<BuildProfile>().unwrap());
2372                            current = vec![];
2373                        }
2374                    }
2375                    L_ANGLE | R_ANGLE => {}
2376                    _ => {
2377                        current.push(token.to_string());
2378                    }
2379                }
2380            }
2381            if !current.is_empty() {
2382                ret.push(current.concat().parse().unwrap());
2383            }
2384            ret
2385        })
2386    }
2387
2388    /// Remove this relation
2389    ///
2390    /// # Example
2391    /// ```
2392    /// use debian_control::lossless::relations::{Relation,Entry};
2393    /// let mut entry: Entry = r"python3-dulwich (>= 0.19.0) | python3-urllib3 (<< 1.26.0)".parse().unwrap();
2394    /// let mut relation = entry.get_relation(0).unwrap();
2395    /// relation.remove();
2396    /// assert_eq!(entry.to_string(), "python3-urllib3 (<< 1.26.0)");
2397    /// ```
2398    pub fn remove(&mut self) {
2399        let is_first = !self
2400            .0
2401            .siblings(Direction::Prev)
2402            .skip(1)
2403            .any(|n| n.kind() == RELATION);
2404        if !is_first {
2405            // Not the first item in the list. Remove whitespace backwards to the previous
2406            // pipe, the pipe and any whitespace until the previous relation
2407            while let Some(n) = self.0.prev_sibling_or_token() {
2408                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2409                    n.detach();
2410                } else if n.kind() == PIPE {
2411                    n.detach();
2412                    break;
2413                } else {
2414                    break;
2415                }
2416            }
2417            while let Some(n) = self.0.prev_sibling_or_token() {
2418                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2419                    n.detach();
2420                } else {
2421                    break;
2422                }
2423            }
2424        } else {
2425            // First item in the list. Remove whitespace up to the pipe, the pipe and anything
2426            // before the next relation
2427            while let Some(n) = self.0.next_sibling_or_token() {
2428                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2429                    n.detach();
2430                } else if n.kind() == PIPE {
2431                    n.detach();
2432                    break;
2433                } else {
2434                    panic!("Unexpected node: {:?}", n);
2435                }
2436            }
2437
2438            while let Some(n) = self.0.next_sibling_or_token() {
2439                if n.kind() == WHITESPACE || n.kind() == NEWLINE {
2440                    n.detach();
2441                } else {
2442                    break;
2443                }
2444            }
2445        }
2446        // If this was the last relation in the entry, remove the entire entry
2447        if let Some(mut parent) = self.0.parent().and_then(Entry::cast) {
2448            if parent.is_empty() {
2449                parent.remove();
2450            } else {
2451                self.0.detach();
2452            }
2453        } else {
2454            self.0.detach();
2455        }
2456    }
2457
2458    /// Set the architectures for this relation
2459    ///
2460    /// # Example
2461    /// ```
2462    /// use debian_control::lossless::relations::Relation;
2463    /// let mut relation = Relation::simple("samba");
2464    /// relation.set_architectures(vec!["amd64", "i386"].into_iter());
2465    /// assert_eq!(relation.to_string(), "samba [amd64 i386]");
2466    /// ```
2467    pub fn set_architectures<'a>(&mut self, architectures: impl Iterator<Item = &'a str>) {
2468        let mut builder = GreenNodeBuilder::new();
2469        builder.start_node(ARCHITECTURES.into());
2470        builder.token(L_BRACKET.into(), "[");
2471        for (i, arch) in architectures.enumerate() {
2472            if i > 0 {
2473                builder.token(WHITESPACE.into(), " ");
2474            }
2475            builder.token(IDENT.into(), arch);
2476        }
2477        builder.token(R_BRACKET.into(), "]");
2478        builder.finish_node();
2479
2480        let node_architectures = self.0.children().find(|n| n.kind() == ARCHITECTURES);
2481        if let Some(node_architectures) = node_architectures {
2482            let new_root = SyntaxNode::new_root_mut(builder.finish());
2483            self.0.splice_children(
2484                node_architectures.index()..node_architectures.index() + 1,
2485                vec![new_root.into()],
2486            );
2487        } else {
2488            let profiles = self.0.children().find(|n| n.kind() == PROFILES);
2489            let idx = if let Some(profiles) = profiles {
2490                profiles.index()
2491            } else {
2492                self.0.children_with_tokens().count()
2493            };
2494            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2495                idx..idx,
2496                vec![
2497                    GreenToken::new(WHITESPACE.into(), " ").into(),
2498                    builder.finish().into(),
2499                ],
2500            ));
2501            if let Some(parent) = self.0.parent() {
2502                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2503                self.0 = parent
2504                    .children_with_tokens()
2505                    .nth(self.0.index())
2506                    .unwrap()
2507                    .clone()
2508                    .into_node()
2509                    .unwrap();
2510            } else {
2511                self.0 = new_root;
2512            }
2513        }
2514    }
2515
2516    /// Add a build profile to this relation
2517    ///
2518    /// # Example
2519    /// ```
2520    /// use debian_control::lossless::relations::Relation;
2521    /// use debian_control::relations::BuildProfile;
2522    /// let mut relation = Relation::simple("samba");
2523    /// relation.add_profile(&[BuildProfile::Disabled("nocheck".to_string())]);
2524    /// assert_eq!(relation.to_string(), "samba <!nocheck>");
2525    /// ```
2526    pub fn add_profile(&mut self, profile: &[BuildProfile]) {
2527        let mut builder = GreenNodeBuilder::new();
2528        builder.start_node(PROFILES.into());
2529        builder.token(L_ANGLE.into(), "<");
2530        for (i, profile) in profile.iter().enumerate() {
2531            if i > 0 {
2532                builder.token(WHITESPACE.into(), " ");
2533            }
2534            match profile {
2535                BuildProfile::Disabled(name) => {
2536                    builder.token(NOT.into(), "!");
2537                    builder.token(IDENT.into(), name.as_str());
2538                }
2539                BuildProfile::Enabled(name) => {
2540                    builder.token(IDENT.into(), name.as_str());
2541                }
2542            }
2543        }
2544        builder.token(R_ANGLE.into(), ">");
2545        builder.finish_node();
2546
2547        let node_profiles = self.0.children().find(|n| n.kind() == PROFILES);
2548        if let Some(node_profiles) = node_profiles {
2549            let new_root = SyntaxNode::new_root_mut(builder.finish());
2550            self.0.splice_children(
2551                node_profiles.index()..node_profiles.index() + 1,
2552                vec![new_root.into()],
2553            );
2554        } else {
2555            let idx = self.0.children_with_tokens().count();
2556            let new_root = SyntaxNode::new_root(self.0.green().splice_children(
2557                idx..idx,
2558                vec![
2559                    GreenToken::new(WHITESPACE.into(), " ").into(),
2560                    builder.finish().into(),
2561                ],
2562            ));
2563            if let Some(parent) = self.0.parent() {
2564                parent.splice_children(self.0.index()..self.0.index() + 1, vec![new_root.into()]);
2565                self.0 = parent
2566                    .children_with_tokens()
2567                    .nth(self.0.index())
2568                    .unwrap()
2569                    .clone()
2570                    .into_node()
2571                    .unwrap();
2572            } else {
2573                self.0 = new_root;
2574            }
2575        }
2576    }
2577
2578    /// Build a new relation
2579    pub fn build(name: &str) -> RelationBuilder {
2580        RelationBuilder::new(name)
2581    }
2582}
2583
2584/// A builder for creating a `Relation`
2585///
2586/// # Example
2587/// ```
2588/// use debian_control::lossless::relations::{Relation};
2589/// use debian_control::relations::VersionConstraint;
2590/// let relation = Relation::build("samba")
2591///    .version_constraint(VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())
2592///    .archqual("any")
2593///    .architectures(vec!["amd64".to_string(), "i386".to_string()])
2594///    .build();
2595/// assert_eq!(relation.to_string(), "samba:any (>= 2.0) [amd64 i386]");
2596/// ```
2597pub struct RelationBuilder {
2598    name: String,
2599    version_constraint: Option<(VersionConstraint, Version)>,
2600    archqual: Option<String>,
2601    architectures: Option<Vec<String>>,
2602    profiles: Vec<Vec<BuildProfile>>,
2603}
2604
2605impl RelationBuilder {
2606    /// Create a new `RelationBuilder` with the given package name
2607    fn new(name: &str) -> Self {
2608        Self {
2609            name: name.to_string(),
2610            version_constraint: None,
2611            archqual: None,
2612            architectures: None,
2613            profiles: vec![],
2614        }
2615    }
2616
2617    /// Set the version constraint for this relation
2618    pub fn version_constraint(mut self, vc: VersionConstraint, version: Version) -> Self {
2619        self.version_constraint = Some((vc, version));
2620        self
2621    }
2622
2623    /// Set the architecture qualifier for this relation
2624    pub fn archqual(mut self, archqual: &str) -> Self {
2625        self.archqual = Some(archqual.to_string());
2626        self
2627    }
2628
2629    /// Set the architectures for this relation
2630    pub fn architectures(mut self, architectures: Vec<String>) -> Self {
2631        self.architectures = Some(architectures);
2632        self
2633    }
2634
2635    /// Set the build profiles for this relation
2636    pub fn profiles(mut self, profiles: Vec<Vec<BuildProfile>>) -> Self {
2637        self.profiles = profiles;
2638        self
2639    }
2640
2641    /// Add a build profile to this relation
2642    pub fn add_profile(mut self, profile: Vec<BuildProfile>) -> Self {
2643        self.profiles.push(profile);
2644        self
2645    }
2646
2647    /// Build the `Relation`
2648    pub fn build(self) -> Relation {
2649        let mut relation = Relation::new(&self.name, self.version_constraint);
2650        if let Some(archqual) = &self.archqual {
2651            relation.set_archqual(archqual);
2652        }
2653        if let Some(architectures) = &self.architectures {
2654            relation.set_architectures(architectures.iter().map(|s| s.as_str()));
2655        }
2656        for profile in &self.profiles {
2657            relation.add_profile(profile);
2658        }
2659        relation
2660    }
2661}
2662
2663impl PartialOrd for Relation {
2664    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2665        // Compare by name first, then by version
2666        let name_cmp = self.name().cmp(&other.name());
2667        if name_cmp != std::cmp::Ordering::Equal {
2668            return Some(name_cmp);
2669        }
2670
2671        let self_version = self.version();
2672        let other_version = other.version();
2673
2674        match (self_version, other_version) {
2675            (Some((self_vc, self_version)), Some((other_vc, other_version))) => {
2676                let vc_cmp = self_vc.cmp(&other_vc);
2677                if vc_cmp != std::cmp::Ordering::Equal {
2678                    return Some(vc_cmp);
2679                }
2680
2681                Some(self_version.cmp(&other_version))
2682            }
2683            (Some(_), None) => Some(std::cmp::Ordering::Greater),
2684            (None, Some(_)) => Some(std::cmp::Ordering::Less),
2685            (None, None) => Some(std::cmp::Ordering::Equal),
2686        }
2687    }
2688}
2689
2690impl Eq for Relation {}
2691
2692impl Ord for Relation {
2693    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2694        self.partial_cmp(other).unwrap()
2695    }
2696}
2697
2698impl std::str::FromStr for Relations {
2699    type Err = String;
2700
2701    fn from_str(s: &str) -> Result<Self, Self::Err> {
2702        let parse = parse(s, false);
2703        if parse.errors.is_empty() {
2704            Ok(parse.root_mut())
2705        } else {
2706            Err(parse.errors.join("\n"))
2707        }
2708    }
2709}
2710
2711impl std::str::FromStr for Entry {
2712    type Err = String;
2713
2714    fn from_str(s: &str) -> Result<Self, Self::Err> {
2715        let root: Relations = s.parse()?;
2716
2717        let mut entries = root.entries();
2718        let entry = if let Some(entry) = entries.next() {
2719            entry
2720        } else {
2721            return Err("No entry found".to_string());
2722        };
2723
2724        if entries.next().is_some() {
2725            return Err("Multiple entries found".to_string());
2726        }
2727
2728        Ok(entry)
2729    }
2730}
2731
2732impl std::str::FromStr for Relation {
2733    type Err = String;
2734
2735    fn from_str(s: &str) -> Result<Self, Self::Err> {
2736        let entry: Entry = s.parse()?;
2737
2738        let mut relations = entry.relations();
2739        let relation = if let Some(relation) = relations.next() {
2740            relation
2741        } else {
2742            return Err("No relation found".to_string());
2743        };
2744
2745        if relations.next().is_some() {
2746            return Err("Multiple relations found".to_string());
2747        }
2748
2749        Ok(relation)
2750    }
2751}
2752
2753impl From<crate::lossy::Relation> for Relation {
2754    fn from(relation: crate::lossy::Relation) -> Self {
2755        let mut builder = Relation::build(&relation.name);
2756
2757        if let Some((vc, version)) = relation.version {
2758            builder = builder.version_constraint(vc, version);
2759        }
2760
2761        if let Some(archqual) = relation.archqual {
2762            builder = builder.archqual(&archqual);
2763        }
2764
2765        if let Some(architectures) = relation.architectures {
2766            builder = builder.architectures(architectures);
2767        }
2768
2769        builder = builder.profiles(relation.profiles);
2770
2771        builder.build()
2772    }
2773}
2774
2775impl From<Relation> for crate::lossy::Relation {
2776    fn from(relation: Relation) -> Self {
2777        crate::lossy::Relation {
2778            name: relation.name(),
2779            version: relation.version(),
2780            archqual: relation.archqual(),
2781            architectures: relation.architectures().map(|a| a.collect()),
2782            profiles: relation.profiles().collect(),
2783        }
2784    }
2785}
2786
2787impl From<Entry> for Vec<crate::lossy::Relation> {
2788    fn from(entry: Entry) -> Self {
2789        entry.relations().map(|r| r.into()).collect()
2790    }
2791}
2792
2793impl From<Vec<crate::lossy::Relation>> for Entry {
2794    fn from(relations: Vec<crate::lossy::Relation>) -> Self {
2795        let relations: Vec<Relation> = relations.into_iter().map(|r| r.into()).collect();
2796        Entry::from(relations)
2797    }
2798}
2799
2800#[cfg(test)]
2801mod tests {
2802    use super::*;
2803
2804    #[test]
2805    fn test_parse() {
2806        let input = "python3-dulwich";
2807        let parsed: Relations = input.parse().unwrap();
2808        assert_eq!(parsed.to_string(), input);
2809        assert_eq!(parsed.entries().count(), 1);
2810        let entry = parsed.entries().next().unwrap();
2811        assert_eq!(entry.to_string(), "python3-dulwich");
2812        assert_eq!(entry.relations().count(), 1);
2813        let relation = entry.relations().next().unwrap();
2814        assert_eq!(relation.to_string(), "python3-dulwich");
2815        assert_eq!(relation.version(), None);
2816
2817        let input = "python3-dulwich (>= 0.20.21)";
2818        let parsed: Relations = input.parse().unwrap();
2819        assert_eq!(parsed.to_string(), input);
2820        assert_eq!(parsed.entries().count(), 1);
2821        let entry = parsed.entries().next().unwrap();
2822        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
2823        assert_eq!(entry.relations().count(), 1);
2824        let relation = entry.relations().next().unwrap();
2825        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
2826        assert_eq!(
2827            relation.version(),
2828            Some((
2829                VersionConstraint::GreaterThanEqual,
2830                "0.20.21".parse().unwrap()
2831            ))
2832        );
2833    }
2834
2835    #[test]
2836    fn test_multiple() {
2837        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
2838        let parsed: Relations = input.parse().unwrap();
2839        assert_eq!(parsed.to_string(), input);
2840        assert_eq!(parsed.entries().count(), 2);
2841        let entry = parsed.entries().next().unwrap();
2842        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
2843        assert_eq!(entry.relations().count(), 1);
2844        let relation = entry.relations().next().unwrap();
2845        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
2846        assert_eq!(
2847            relation.version(),
2848            Some((
2849                VersionConstraint::GreaterThanEqual,
2850                "0.20.21".parse().unwrap()
2851            ))
2852        );
2853        let entry = parsed.entries().nth(1).unwrap();
2854        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.21)");
2855        assert_eq!(entry.relations().count(), 1);
2856        let relation = entry.relations().next().unwrap();
2857        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
2858        assert_eq!(
2859            relation.version(),
2860            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
2861        );
2862    }
2863
2864    #[test]
2865    fn test_architectures() {
2866        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
2867        let parsed: Relations = input.parse().unwrap();
2868        assert_eq!(parsed.to_string(), input);
2869        assert_eq!(parsed.entries().count(), 1);
2870        let entry = parsed.entries().next().unwrap();
2871        assert_eq!(
2872            entry.to_string(),
2873            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
2874        );
2875        assert_eq!(entry.relations().count(), 1);
2876        let relation = entry.relations().next().unwrap();
2877        assert_eq!(
2878            relation.to_string(),
2879            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
2880        );
2881        assert_eq!(relation.version(), None);
2882        assert_eq!(
2883            relation.architectures().unwrap().collect::<Vec<_>>(),
2884            vec![
2885                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
2886            ]
2887            .into_iter()
2888            .map(|s| s.to_string())
2889            .collect::<Vec<_>>()
2890        );
2891    }
2892
2893    #[test]
2894    fn test_profiles() {
2895        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
2896        let parsed: Relations = input.parse().unwrap();
2897        assert_eq!(parsed.to_string(), input);
2898        assert_eq!(parsed.entries().count(), 2);
2899        let entry = parsed.entries().next().unwrap();
2900        assert_eq!(
2901            entry.to_string(),
2902            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
2903        );
2904        assert_eq!(entry.relations().count(), 1);
2905        let relation = entry.relations().next().unwrap();
2906        assert_eq!(
2907            relation.to_string(),
2908            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
2909        );
2910        assert_eq!(
2911            relation.version(),
2912            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
2913        );
2914        assert_eq!(
2915            relation.architectures().unwrap().collect::<Vec<_>>(),
2916            vec!["i386", "arm"]
2917                .into_iter()
2918                .map(|s| s.to_string())
2919                .collect::<Vec<_>>()
2920        );
2921        assert_eq!(
2922            relation.profiles().collect::<Vec<_>>(),
2923            vec![
2924                vec![BuildProfile::Disabled("nocheck".to_string())],
2925                vec![BuildProfile::Disabled("cross".to_string())]
2926            ]
2927        );
2928    }
2929
2930    #[test]
2931    fn test_substvar() {
2932        let input = "${shlibs:Depends}";
2933
2934        let (parsed, errors) = Relations::parse_relaxed(input, true);
2935        assert_eq!(errors, Vec::<String>::new());
2936        assert_eq!(parsed.to_string(), input);
2937        assert_eq!(parsed.entries().count(), 0);
2938
2939        assert_eq!(
2940            parsed.substvars().collect::<Vec<_>>(),
2941            vec!["${shlibs:Depends}"]
2942        );
2943    }
2944
2945    #[test]
2946    fn test_new() {
2947        let r = Relation::new(
2948            "samba",
2949            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
2950        );
2951
2952        assert_eq!(r.to_string(), "samba (>= 2.0)");
2953    }
2954
2955    #[test]
2956    fn test_drop_constraint() {
2957        let mut r = Relation::new(
2958            "samba",
2959            Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())),
2960        );
2961
2962        r.drop_constraint();
2963
2964        assert_eq!(r.to_string(), "samba");
2965    }
2966
2967    #[test]
2968    fn test_simple() {
2969        let r = Relation::simple("samba");
2970
2971        assert_eq!(r.to_string(), "samba");
2972    }
2973
2974    #[test]
2975    fn test_remove_first_entry() {
2976        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
2977            .parse()
2978            .unwrap();
2979        let removed = rels.remove_entry(0);
2980        assert_eq!(removed.to_string(), "python3-dulwich (>= 0.20.21)");
2981        assert_eq!(rels.to_string(), "python3-dulwich (<< 0.21)");
2982    }
2983
2984    #[test]
2985    fn test_remove_last_entry() {
2986        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
2987            .parse()
2988            .unwrap();
2989        rels.remove_entry(1);
2990        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
2991    }
2992
2993    #[test]
2994    fn test_remove_middle() {
2995        let mut rels: Relations =
2996            r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21), python3-dulwich (<< 0.22)"#
2997                .parse()
2998                .unwrap();
2999        rels.remove_entry(1);
3000        assert_eq!(
3001            rels.to_string(),
3002            "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.22)"
3003        );
3004    }
3005
3006    #[test]
3007    fn test_remove_added() {
3008        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3009        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3010        rels.push(entry);
3011        rels.remove_entry(1);
3012        assert_eq!(rels.to_string(), "python3-dulwich (>= 0.20.21)");
3013    }
3014
3015    #[test]
3016    fn test_push() {
3017        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21)"#.parse().unwrap();
3018        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3019        rels.push(entry);
3020        assert_eq!(
3021            rels.to_string(),
3022            "python3-dulwich (>= 0.20.21), python3-dulwich"
3023        );
3024    }
3025
3026    #[test]
3027    fn test_insert_with_custom_separator() {
3028        let mut rels: Relations = "python3".parse().unwrap();
3029        let entry = Entry::from(vec![Relation::simple("debhelper")]);
3030        rels.insert_with_separator(1, entry, Some("\n "));
3031        assert_eq!(rels.to_string(), "python3,\n debhelper");
3032    }
3033
3034    #[test]
3035    fn test_insert_with_wrap_and_sort_separator() {
3036        let mut rels: Relations = "python3".parse().unwrap();
3037        let entry = Entry::from(vec![Relation::simple("rustc")]);
3038        // Simulate wrap-and-sort -a style with field name "Depends: " (9 chars)
3039        rels.insert_with_separator(1, entry, Some("\n         "));
3040        assert_eq!(rels.to_string(), "python3,\n         rustc");
3041    }
3042
3043    #[test]
3044    fn test_push_from_empty() {
3045        let mut rels: Relations = "".parse().unwrap();
3046        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3047        rels.push(entry);
3048        assert_eq!(rels.to_string(), "python3-dulwich");
3049    }
3050
3051    #[test]
3052    fn test_insert() {
3053        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3054            .parse()
3055            .unwrap();
3056        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3057        rels.insert(1, entry);
3058        assert_eq!(
3059            rels.to_string(),
3060            "python3-dulwich (>= 0.20.21), python3-dulwich, python3-dulwich (<< 0.21)"
3061        );
3062    }
3063
3064    #[test]
3065    fn test_insert_at_start() {
3066        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3067            .parse()
3068            .unwrap();
3069        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3070        rels.insert(0, entry);
3071        assert_eq!(
3072            rels.to_string(),
3073            "python3-dulwich, python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3074        );
3075    }
3076
3077    #[test]
3078    fn test_insert_after_error() {
3079        let (mut rels, errors) = Relations::parse_relaxed("@foo@, debhelper (>= 1.0)", false);
3080        assert_eq!(
3081            errors,
3082            vec![
3083                "expected $ or identifier but got ERROR",
3084                "expected comma or end of file but got Some(IDENT)",
3085                "expected $ or identifier but got ERROR"
3086            ]
3087        );
3088        let entry = Entry::from(vec![Relation::simple("bar")]);
3089        rels.push(entry);
3090        assert_eq!(rels.to_string(), "@foo@, debhelper (>= 1.0), bar");
3091    }
3092
3093    #[test]
3094    fn test_insert_before_error() {
3095        let (mut rels, errors) = Relations::parse_relaxed("debhelper (>= 1.0), @foo@, bla", false);
3096        assert_eq!(
3097            errors,
3098            vec![
3099                "expected $ or identifier but got ERROR",
3100                "expected comma or end of file but got Some(IDENT)",
3101                "expected $ or identifier but got ERROR"
3102            ]
3103        );
3104        let entry = Entry::from(vec![Relation::simple("bar")]);
3105        rels.insert(0, entry);
3106        assert_eq!(rels.to_string(), "bar, debhelper (>= 1.0), @foo@, bla");
3107    }
3108
3109    #[test]
3110    fn test_replace() {
3111        let mut rels: Relations = r#"python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"#
3112            .parse()
3113            .unwrap();
3114        let entry = Entry::from(vec![Relation::simple("python3-dulwich")]);
3115        rels.replace(1, entry);
3116        assert_eq!(
3117            rels.to_string(),
3118            "python3-dulwich (>= 0.20.21), python3-dulwich"
3119        );
3120    }
3121
3122    #[test]
3123    fn test_relation_from_entries() {
3124        let entries = vec![
3125            Entry::from(vec![Relation::simple("python3-dulwich")]),
3126            Entry::from(vec![Relation::simple("python3-breezy")]),
3127        ];
3128        let rels: Relations = entries.into();
3129        assert_eq!(rels.entries().count(), 2);
3130        assert_eq!(rels.to_string(), "python3-dulwich, python3-breezy");
3131    }
3132
3133    #[test]
3134    fn test_entry_from_relations() {
3135        let relations = vec![
3136            Relation::simple("python3-dulwich"),
3137            Relation::simple("python3-breezy"),
3138        ];
3139        let entry: Entry = relations.into();
3140        assert_eq!(entry.relations().count(), 2);
3141        assert_eq!(entry.to_string(), "python3-dulwich | python3-breezy");
3142    }
3143
3144    #[test]
3145    fn test_parse_entry() {
3146        let parsed: Entry = "python3-dulwich (>= 0.20.21) | bar".parse().unwrap();
3147        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21) | bar");
3148        assert_eq!(parsed.relations().count(), 2);
3149
3150        assert_eq!(
3151            "foo, bar".parse::<Entry>().unwrap_err(),
3152            "Multiple entries found"
3153        );
3154        assert_eq!("".parse::<Entry>().unwrap_err(), "No entry found");
3155    }
3156
3157    #[test]
3158    fn test_parse_relation() {
3159        let parsed: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3160        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
3161        assert_eq!(
3162            parsed.version(),
3163            Some((
3164                VersionConstraint::GreaterThanEqual,
3165                "0.20.21".parse().unwrap()
3166            ))
3167        );
3168        assert_eq!(
3169            "foo | bar".parse::<Relation>().unwrap_err(),
3170            "Multiple relations found"
3171        );
3172        assert_eq!("".parse::<Relation>().unwrap_err(), "No entry found");
3173    }
3174
3175    #[test]
3176    fn test_special() {
3177        let parsed: Relation = "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3178            .parse()
3179            .unwrap();
3180        assert_eq!(
3181            parsed.to_string(),
3182            "librust-breezyshim+dirty-tracker-dev:amd64 (>= 0.1.138-~~)"
3183        );
3184        assert_eq!(
3185            parsed.version(),
3186            Some((
3187                VersionConstraint::GreaterThanEqual,
3188                "0.1.138-~~".parse().unwrap()
3189            ))
3190        );
3191        assert_eq!(parsed.archqual(), Some("amd64".to_string()));
3192        assert_eq!(parsed.name(), "librust-breezyshim+dirty-tracker-dev");
3193    }
3194
3195    #[test]
3196    fn test_relations_satisfied_by() {
3197        let rels: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3198            .parse()
3199            .unwrap();
3200        let satisfied = |name: &str| -> Option<debversion::Version> {
3201            match name {
3202                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3203                _ => None,
3204            }
3205        };
3206        assert!(rels.satisfied_by(satisfied));
3207
3208        let satisfied = |name: &str| match name {
3209            "python3-dulwich" => Some("0.21".parse().unwrap()),
3210            _ => None,
3211        };
3212        assert!(!rels.satisfied_by(satisfied));
3213
3214        let satisfied = |name: &str| match name {
3215            "python3-dulwich" => Some("0.20.20".parse().unwrap()),
3216            _ => None,
3217        };
3218        assert!(!rels.satisfied_by(satisfied));
3219    }
3220
3221    #[test]
3222    fn test_entry_satisfied_by() {
3223        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3224            .parse()
3225            .unwrap();
3226        let satisfied = |name: &str| -> Option<debversion::Version> {
3227            match name {
3228                "python3-dulwich" => Some("0.20.21".parse().unwrap()),
3229                _ => None,
3230            }
3231        };
3232        assert!(entry.satisfied_by(satisfied));
3233        let satisfied = |name: &str| -> Option<debversion::Version> {
3234            match name {
3235                "python3-dulwich" => Some("0.18".parse().unwrap()),
3236                _ => None,
3237            }
3238        };
3239        assert!(!entry.satisfied_by(satisfied));
3240    }
3241
3242    #[test]
3243    fn test_wrap_and_sort_relation() {
3244        let relation: Relation = "   python3-dulwich   (>= 11) [  amd64 ] <  lala>"
3245            .parse()
3246            .unwrap();
3247
3248        let wrapped = relation.wrap_and_sort();
3249
3250        assert_eq!(
3251            wrapped.to_string(),
3252            "python3-dulwich (>= 11) [amd64] <lala>"
3253        );
3254    }
3255
3256    #[test]
3257    fn test_wrap_and_sort_relations() {
3258        let entry: Relations =
3259            "python3-dulwich (>= 0.20.21)   | bar, \n\n\n\npython3-dulwich (<< 0.21)"
3260                .parse()
3261                .unwrap();
3262
3263        let wrapped = entry.wrap_and_sort();
3264
3265        assert_eq!(
3266            wrapped.to_string(),
3267            "bar | python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3268        );
3269    }
3270
3271    #[cfg(feature = "serde")]
3272    #[test]
3273    fn test_serialize_relations() {
3274        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3275            .parse()
3276            .unwrap();
3277        let serialized = serde_json::to_string(&relations).unwrap();
3278        assert_eq!(
3279            serialized,
3280            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
3281        );
3282    }
3283
3284    #[cfg(feature = "serde")]
3285    #[test]
3286    fn test_deserialize_relations() {
3287        let relations: Relations = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)"
3288            .parse()
3289            .unwrap();
3290        let serialized = serde_json::to_string(&relations).unwrap();
3291        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
3292        assert_eq!(deserialized.to_string(), relations.to_string());
3293    }
3294
3295    #[cfg(feature = "serde")]
3296    #[test]
3297    fn test_serialize_relation() {
3298        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3299        let serialized = serde_json::to_string(&relation).unwrap();
3300        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
3301    }
3302
3303    #[cfg(feature = "serde")]
3304    #[test]
3305    fn test_deserialize_relation() {
3306        let relation: Relation = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3307        let serialized = serde_json::to_string(&relation).unwrap();
3308        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
3309        assert_eq!(deserialized.to_string(), relation.to_string());
3310    }
3311
3312    #[cfg(feature = "serde")]
3313    #[test]
3314    fn test_serialize_entry() {
3315        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3316            .parse()
3317            .unwrap();
3318        let serialized = serde_json::to_string(&entry).unwrap();
3319        assert_eq!(
3320            serialized,
3321            r#""python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)""#
3322        );
3323    }
3324
3325    #[cfg(feature = "serde")]
3326    #[test]
3327    fn test_deserialize_entry() {
3328        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3329            .parse()
3330            .unwrap();
3331        let serialized = serde_json::to_string(&entry).unwrap();
3332        let deserialized: Entry = serde_json::from_str(&serialized).unwrap();
3333        assert_eq!(deserialized.to_string(), entry.to_string());
3334    }
3335
3336    #[test]
3337    fn test_remove_first_relation() {
3338        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3339            .parse()
3340            .unwrap();
3341        let mut rel = entry.relations().next().unwrap();
3342        rel.remove();
3343        assert_eq!(entry.to_string(), "python3-dulwich (<< 0.18)");
3344    }
3345
3346    #[test]
3347    fn test_remove_last_relation() {
3348        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3349            .parse()
3350            .unwrap();
3351        let mut rel = entry.relations().nth(1).unwrap();
3352        rel.remove();
3353        assert_eq!(entry.to_string(), "python3-dulwich (>= 0.20.21)");
3354    }
3355
3356    #[test]
3357    fn test_remove_only_relation() {
3358        let entry: Entry = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3359        let mut rel = entry.relations().next().unwrap();
3360        rel.remove();
3361        assert_eq!(entry.to_string(), "");
3362    }
3363
3364    #[test]
3365    fn test_relations_is_empty() {
3366        let entry: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3367        assert!(!entry.is_empty());
3368        assert_eq!(1, entry.len());
3369        let mut rel = entry.entries().next().unwrap();
3370        rel.remove();
3371        assert!(entry.is_empty());
3372        assert_eq!(0, entry.len());
3373    }
3374
3375    #[test]
3376    fn test_entry_is_empty() {
3377        let entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3378            .parse()
3379            .unwrap();
3380        assert!(!entry.is_empty());
3381        assert_eq!(2, entry.len());
3382        let mut rel = entry.relations().next().unwrap();
3383        rel.remove();
3384        assert!(!entry.is_empty());
3385        assert_eq!(1, entry.len());
3386        let mut rel = entry.relations().next().unwrap();
3387        rel.remove();
3388        assert!(entry.is_empty());
3389        assert_eq!(0, entry.len());
3390    }
3391
3392    #[test]
3393    fn test_relation_set_version() {
3394        let mut rel: Relation = "samba".parse().unwrap();
3395        rel.set_version(None);
3396        assert_eq!("samba", rel.to_string());
3397        rel.set_version(Some((
3398            VersionConstraint::GreaterThanEqual,
3399            "2.0".parse().unwrap(),
3400        )));
3401        assert_eq!("samba (>= 2.0)", rel.to_string());
3402        rel.set_version(None);
3403        assert_eq!("samba", rel.to_string());
3404        rel.set_version(Some((
3405            VersionConstraint::GreaterThanEqual,
3406            "2.0".parse().unwrap(),
3407        )));
3408        rel.set_version(Some((
3409            VersionConstraint::GreaterThanEqual,
3410            "1.1".parse().unwrap(),
3411        )));
3412        assert_eq!("samba (>= 1.1)", rel.to_string());
3413    }
3414
3415    #[test]
3416    fn test_replace_relation() {
3417        let mut entry: Entry = "python3-dulwich (>= 0.20.21) | python3-dulwich (<< 0.18)"
3418            .parse()
3419            .unwrap();
3420        let new_rel = Relation::simple("python3-breezy");
3421        entry.replace(0, new_rel);
3422        assert_eq!(
3423            entry.to_string(),
3424            "python3-breezy | python3-dulwich (<< 0.18)"
3425        );
3426    }
3427
3428    #[test]
3429    fn test_entry_push_relation() {
3430        let relations: Relations = "python3-dulwich (>= 0.20.21)".parse().unwrap();
3431        let new_rel = Relation::simple("python3-breezy");
3432        let mut entry = relations.entries().next().unwrap();
3433        entry.push(new_rel);
3434        assert_eq!(
3435            entry.to_string(),
3436            "python3-dulwich (>= 0.20.21) | python3-breezy"
3437        );
3438        assert_eq!(
3439            relations.to_string(),
3440            "python3-dulwich (>= 0.20.21) | python3-breezy"
3441        );
3442    }
3443
3444    #[test]
3445    fn test_relations_remove_empty_entry() {
3446        let (mut relations, errors) = Relations::parse_relaxed("foo, , bar, ", false);
3447        assert_eq!(errors, Vec::<String>::new());
3448        assert_eq!(relations.to_string(), "foo, , bar, ");
3449        assert_eq!(relations.len(), 2);
3450        assert_eq!(
3451            relations.entries().next().unwrap().to_string(),
3452            "foo".to_string()
3453        );
3454        assert_eq!(
3455            relations.entries().nth(1).unwrap().to_string(),
3456            "bar".to_string()
3457        );
3458        relations.remove_entry(1);
3459        assert_eq!(relations.to_string(), "foo, , ");
3460    }
3461
3462    #[test]
3463    fn test_entry_remove_relation() {
3464        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3465        let removed = entry.remove_relation(0);
3466        assert_eq!(removed.to_string(), "python3-dulwich");
3467        assert_eq!(entry.to_string(), "samba");
3468    }
3469
3470    #[test]
3471    fn test_wrap_and_sort_removes_empty_entries() {
3472        let relations: Relations = "foo, , bar, ".parse().unwrap();
3473        let wrapped = relations.wrap_and_sort();
3474        assert_eq!(wrapped.to_string(), "bar, foo");
3475    }
3476
3477    #[test]
3478    fn test_set_archqual() {
3479        let entry: Entry = "python3-dulwich | samba".parse().unwrap();
3480        let mut rel = entry.relations().next().unwrap();
3481        rel.set_archqual("amd64");
3482        assert_eq!(rel.to_string(), "python3-dulwich:amd64");
3483        assert_eq!(rel.archqual(), Some("amd64".to_string()));
3484        assert_eq!(entry.to_string(), "python3-dulwich:amd64 | samba");
3485        rel.set_archqual("i386");
3486        assert_eq!(rel.to_string(), "python3-dulwich:i386");
3487        assert_eq!(rel.archqual(), Some("i386".to_string()));
3488        assert_eq!(entry.to_string(), "python3-dulwich:i386 | samba");
3489    }
3490
3491    #[test]
3492    fn test_set_architectures() {
3493        let mut relation = Relation::simple("samba");
3494        relation.set_architectures(vec!["amd64", "i386"].into_iter());
3495        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3496    }
3497
3498    #[test]
3499    fn test_relation_builder_no_architectures() {
3500        // Test that building a relation without architectures doesn't add empty brackets
3501        let relation = Relation::build("debhelper").build();
3502        assert_eq!(relation.to_string(), "debhelper");
3503    }
3504
3505    #[test]
3506    fn test_relation_builder_with_architectures() {
3507        // Test that building a relation with architectures works correctly
3508        let relation = Relation::build("samba")
3509            .architectures(vec!["amd64".to_string(), "i386".to_string()])
3510            .build();
3511        assert_eq!(relation.to_string(), "samba [amd64 i386]");
3512    }
3513
3514    #[test]
3515    fn test_ensure_minimum_version_add_new() {
3516        let mut relations: Relations = "python3".parse().unwrap();
3517        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3518        assert_eq!(relations.to_string(), "python3, debhelper (>= 12)");
3519    }
3520
3521    #[test]
3522    fn test_ensure_minimum_version_update() {
3523        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3524        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3525        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3526    }
3527
3528    #[test]
3529    fn test_ensure_minimum_version_no_change() {
3530        let mut relations: Relations = "debhelper (>= 13)".parse().unwrap();
3531        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3532        assert_eq!(relations.to_string(), "debhelper (>= 13)");
3533    }
3534
3535    #[test]
3536    fn test_ensure_minimum_version_no_version() {
3537        let mut relations: Relations = "debhelper".parse().unwrap();
3538        relations.ensure_minimum_version("debhelper", &"12".parse().unwrap());
3539        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3540    }
3541
3542    #[test]
3543    fn test_ensure_minimum_version_preserves_newline() {
3544        // Test that newline after the field name is preserved
3545        // This is the format often used in Debian control files:
3546        // Build-Depends:
3547        //  debhelper (>= 9),
3548        //  pkg-config
3549        let input = "\n debhelper (>= 9),\n pkg-config,\n uuid-dev";
3550        let mut relations: Relations = input.parse().unwrap();
3551        relations.ensure_minimum_version("debhelper", &"12~".parse().unwrap());
3552        let result = relations.to_string();
3553
3554        // The newline before the first entry should be preserved
3555        assert!(
3556            result.starts_with('\n'),
3557            "Expected result to start with newline, got: {:?}",
3558            result
3559        );
3560        assert_eq!(result, "\n debhelper (>= 12~),\n pkg-config,\n uuid-dev");
3561    }
3562
3563    #[test]
3564    fn test_ensure_minimum_version_preserves_newline_in_control() {
3565        // Test the full scenario from the bug report
3566        use crate::lossless::Control;
3567        use std::str::FromStr;
3568
3569        let input = r#"Source: f2fs-tools
3570Section: admin
3571Priority: optional
3572Maintainer: Test <test@example.com>
3573Build-Depends:
3574 debhelper (>= 9),
3575 pkg-config,
3576 uuid-dev
3577
3578Package: f2fs-tools
3579Description: test
3580"#;
3581
3582        let control = Control::from_str(input).unwrap();
3583        let mut source = control.source().unwrap();
3584        let mut build_depends = source.build_depends().unwrap();
3585
3586        let version = Version::from_str("12~").unwrap();
3587        build_depends.ensure_minimum_version("debhelper", &version);
3588
3589        source.set_build_depends(&build_depends);
3590
3591        let output = control.to_string();
3592
3593        // Check that the formatting is preserved - the newline after "Build-Depends:" should still be there
3594        assert!(
3595            output.contains("Build-Depends:\n debhelper (>= 12~)"),
3596            "Expected 'Build-Depends:\\n debhelper (>= 12~)' but got:\n{}",
3597            output
3598        );
3599    }
3600
3601    #[test]
3602    fn test_ensure_exact_version_add_new() {
3603        let mut relations: Relations = "python3".parse().unwrap();
3604        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3605        assert_eq!(relations.to_string(), "python3, debhelper (= 12)");
3606    }
3607
3608    #[test]
3609    fn test_ensure_exact_version_update() {
3610        let mut relations: Relations = "debhelper (>= 9)".parse().unwrap();
3611        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3612        assert_eq!(relations.to_string(), "debhelper (= 12)");
3613    }
3614
3615    #[test]
3616    fn test_ensure_exact_version_no_change() {
3617        let mut relations: Relations = "debhelper (= 12)".parse().unwrap();
3618        relations.ensure_exact_version("debhelper", &"12".parse().unwrap());
3619        assert_eq!(relations.to_string(), "debhelper (= 12)");
3620    }
3621
3622    #[test]
3623    fn test_ensure_some_version_add_new() {
3624        let mut relations: Relations = "python3".parse().unwrap();
3625        relations.ensure_some_version("debhelper");
3626        assert_eq!(relations.to_string(), "python3, debhelper");
3627    }
3628
3629    #[test]
3630    fn test_ensure_some_version_exists_with_version() {
3631        let mut relations: Relations = "debhelper (>= 12)".parse().unwrap();
3632        relations.ensure_some_version("debhelper");
3633        assert_eq!(relations.to_string(), "debhelper (>= 12)");
3634    }
3635
3636    #[test]
3637    fn test_ensure_some_version_exists_no_version() {
3638        let mut relations: Relations = "debhelper".parse().unwrap();
3639        relations.ensure_some_version("debhelper");
3640        assert_eq!(relations.to_string(), "debhelper");
3641    }
3642
3643    #[test]
3644    fn test_ensure_substvar() {
3645        let mut relations: Relations = "python3".parse().unwrap();
3646        relations.ensure_substvar("${misc:Depends}").unwrap();
3647        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
3648    }
3649
3650    #[test]
3651    fn test_ensure_substvar_already_exists() {
3652        let (mut relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}", true);
3653        relations.ensure_substvar("${misc:Depends}").unwrap();
3654        assert_eq!(relations.to_string(), "python3, ${misc:Depends}");
3655    }
3656
3657    #[test]
3658    fn test_ensure_substvar_empty_relations() {
3659        let mut relations: Relations = Relations::new();
3660        relations.ensure_substvar("${misc:Depends}").unwrap();
3661        assert_eq!(relations.to_string(), "${misc:Depends}");
3662    }
3663
3664    #[test]
3665    fn test_ensure_substvar_preserves_whitespace() {
3666        // Test with non-standard whitespace (multiple spaces)
3667        let (mut relations, _) = Relations::parse_relaxed("python3,  rustc", false);
3668        relations.ensure_substvar("${misc:Depends}").unwrap();
3669        // Should preserve the double-space pattern
3670        assert_eq!(relations.to_string(), "python3,  rustc,  ${misc:Depends}");
3671    }
3672
3673    #[test]
3674    fn test_filter_entries_basic() {
3675        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
3676        relations.filter_entries(|entry| entry.relations().any(|r| r.name().starts_with("python")));
3677        assert_eq!(relations.to_string(), "python3");
3678    }
3679
3680    #[test]
3681    fn test_filter_entries_keep_all() {
3682        let mut relations: Relations = "python3, debhelper".parse().unwrap();
3683        relations.filter_entries(|_| true);
3684        assert_eq!(relations.to_string(), "python3, debhelper");
3685    }
3686
3687    #[test]
3688    fn test_filter_entries_remove_all() {
3689        let mut relations: Relations = "python3, debhelper".parse().unwrap();
3690        relations.filter_entries(|_| false);
3691        assert_eq!(relations.to_string(), "");
3692    }
3693
3694    #[test]
3695    fn test_filter_entries_keep_middle() {
3696        let mut relations: Relations = "aaa, bbb, ccc".parse().unwrap();
3697        relations.filter_entries(|entry| entry.relations().any(|r| r.name() == "bbb"));
3698        assert_eq!(relations.to_string(), "bbb");
3699    }
3700
3701    // Tests for new convenience methods
3702
3703    #[test]
3704    fn test_is_sorted_wrap_and_sort_order() {
3705        // Sorted according to WrapAndSortOrder
3706        let relations: Relations = "debhelper, python3, rustc".parse().unwrap();
3707        assert!(relations.is_sorted(&WrapAndSortOrder));
3708
3709        // Not sorted
3710        let relations: Relations = "rustc, debhelper, python3".parse().unwrap();
3711        assert!(!relations.is_sorted(&WrapAndSortOrder));
3712
3713        // Build systems first (sorted alphabetically within their group)
3714        let (relations, _) =
3715            Relations::parse_relaxed("cdbs, debhelper-compat, python3, ${misc:Depends}", true);
3716        assert!(relations.is_sorted(&WrapAndSortOrder));
3717    }
3718
3719    #[test]
3720    fn test_is_sorted_default_order() {
3721        // Sorted alphabetically
3722        let relations: Relations = "aaa, bbb, ccc".parse().unwrap();
3723        assert!(relations.is_sorted(&DefaultSortingOrder));
3724
3725        // Not sorted
3726        let relations: Relations = "ccc, aaa, bbb".parse().unwrap();
3727        assert!(!relations.is_sorted(&DefaultSortingOrder));
3728
3729        // Special items at end
3730        let (relations, _) = Relations::parse_relaxed("aaa, bbb, ${misc:Depends}", true);
3731        assert!(relations.is_sorted(&DefaultSortingOrder));
3732    }
3733
3734    #[test]
3735    fn test_is_sorted_with_substvars() {
3736        // Substvars should be ignored by DefaultSortingOrder
3737        let (relations, _) = Relations::parse_relaxed("python3, ${misc:Depends}, rustc", true);
3738        // This is considered sorted because ${misc:Depends} is ignored
3739        assert!(relations.is_sorted(&DefaultSortingOrder));
3740    }
3741
3742    #[test]
3743    fn test_drop_dependency_exists() {
3744        let mut relations: Relations = "python3, debhelper, rustc".parse().unwrap();
3745        assert!(relations.drop_dependency("debhelper"));
3746        assert_eq!(relations.to_string(), "python3, rustc");
3747    }
3748
3749    #[test]
3750    fn test_drop_dependency_not_exists() {
3751        let mut relations: Relations = "python3, rustc".parse().unwrap();
3752        assert!(!relations.drop_dependency("nonexistent"));
3753        assert_eq!(relations.to_string(), "python3, rustc");
3754    }
3755
3756    #[test]
3757    fn test_drop_dependency_only_item() {
3758        let mut relations: Relations = "python3".parse().unwrap();
3759        assert!(relations.drop_dependency("python3"));
3760        assert_eq!(relations.to_string(), "");
3761    }
3762
3763    #[test]
3764    fn test_add_dependency_to_empty() {
3765        let mut relations: Relations = "".parse().unwrap();
3766        let entry = Entry::from(Relation::simple("python3"));
3767        relations.add_dependency(entry, None);
3768        assert_eq!(relations.to_string(), "python3");
3769    }
3770
3771    #[test]
3772    fn test_add_dependency_sorted_position() {
3773        let mut relations: Relations = "debhelper, rustc".parse().unwrap();
3774        let entry = Entry::from(Relation::simple("python3"));
3775        relations.add_dependency(entry, None);
3776        // Should be inserted in sorted position
3777        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
3778    }
3779
3780    #[test]
3781    fn test_add_dependency_explicit_position() {
3782        let mut relations: Relations = "python3, rustc".parse().unwrap();
3783        let entry = Entry::from(Relation::simple("debhelper"));
3784        relations.add_dependency(entry, Some(0));
3785        assert_eq!(relations.to_string(), "debhelper, python3, rustc");
3786    }
3787
3788    #[test]
3789    fn test_add_dependency_build_system_first() {
3790        let mut relations: Relations = "python3, rustc".parse().unwrap();
3791        let entry = Entry::from(Relation::simple("debhelper-compat"));
3792        relations.add_dependency(entry, None);
3793        // debhelper-compat should be inserted first (build system)
3794        assert_eq!(relations.to_string(), "debhelper-compat, python3, rustc");
3795    }
3796
3797    #[test]
3798    fn test_add_dependency_at_end() {
3799        let mut relations: Relations = "debhelper, python3".parse().unwrap();
3800        let entry = Entry::from(Relation::simple("zzz-package"));
3801        relations.add_dependency(entry, None);
3802        // Should be added at the end (alphabetically after python3)
3803        assert_eq!(relations.to_string(), "debhelper, python3, zzz-package");
3804    }
3805
3806    #[test]
3807    fn test_add_dependency_to_single_entry() {
3808        // Regression test: ensure comma is added when inserting into single-entry Relations
3809        let mut relations: Relations = "python3-dulwich".parse().unwrap();
3810        let entry: Entry = "debhelper-compat (= 12)".parse().unwrap();
3811        relations.add_dependency(entry, None);
3812        // Should insert with comma separator
3813        assert_eq!(
3814            relations.to_string(),
3815            "debhelper-compat (= 12), python3-dulwich"
3816        );
3817    }
3818
3819    #[test]
3820    fn test_get_relation_exists() {
3821        let relations: Relations = "python3, debhelper (>= 12), rustc".parse().unwrap();
3822        let result = relations.get_relation("debhelper");
3823        assert!(result.is_ok());
3824        let (idx, entry) = result.unwrap();
3825        assert_eq!(idx, 1);
3826        assert_eq!(entry.to_string(), "debhelper (>= 12)");
3827    }
3828
3829    #[test]
3830    fn test_get_relation_not_exists() {
3831        let relations: Relations = "python3, rustc".parse().unwrap();
3832        let result = relations.get_relation("nonexistent");
3833        assert_eq!(result, Err("Package nonexistent not found".to_string()));
3834    }
3835
3836    #[test]
3837    fn test_get_relation_complex_rule() {
3838        let relations: Relations = "python3 | python3-minimal, rustc".parse().unwrap();
3839        let result = relations.get_relation("python3");
3840        assert_eq!(
3841            result,
3842            Err("Complex rule for python3, aborting".to_string())
3843        );
3844    }
3845
3846    #[test]
3847    fn test_iter_relations_for_simple() {
3848        let relations: Relations = "python3, debhelper, python3-dev".parse().unwrap();
3849        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
3850        assert_eq!(entries.len(), 1);
3851        assert_eq!(entries[0].0, 0);
3852        assert_eq!(entries[0].1.to_string(), "python3");
3853    }
3854
3855    #[test]
3856    fn test_iter_relations_for_alternatives() {
3857        let relations: Relations = "python3 | python3-minimal, python3-dev".parse().unwrap();
3858        let entries: Vec<_> = relations.iter_relations_for("python3").collect();
3859        // Should find both the alternative entry and python3-dev is not included
3860        assert_eq!(entries.len(), 1);
3861        assert_eq!(entries[0].0, 0);
3862    }
3863
3864    #[test]
3865    fn test_iter_relations_for_not_found() {
3866        let relations: Relations = "python3, rustc".parse().unwrap();
3867        let entries: Vec<_> = relations.iter_relations_for("debhelper").collect();
3868        assert_eq!(entries.len(), 0);
3869    }
3870
3871    #[test]
3872    fn test_has_relation_exists() {
3873        let relations: Relations = "python3, debhelper, rustc".parse().unwrap();
3874        assert!(relations.has_relation("debhelper"));
3875        assert!(relations.has_relation("python3"));
3876        assert!(relations.has_relation("rustc"));
3877    }
3878
3879    #[test]
3880    fn test_has_relation_not_exists() {
3881        let relations: Relations = "python3, rustc".parse().unwrap();
3882        assert!(!relations.has_relation("debhelper"));
3883    }
3884
3885    #[test]
3886    fn test_has_relation_in_alternative() {
3887        let relations: Relations = "python3 | python3-minimal".parse().unwrap();
3888        assert!(relations.has_relation("python3"));
3889        assert!(relations.has_relation("python3-minimal"));
3890    }
3891
3892    #[test]
3893    fn test_sorting_order_wrap_and_sort_build_systems() {
3894        let order = WrapAndSortOrder;
3895        // Build systems should come before regular packages
3896        assert!(order.lt("debhelper", "python3"));
3897        assert!(order.lt("debhelper-compat", "rustc"));
3898        assert!(order.lt("cdbs", "aaa"));
3899        assert!(order.lt("dh-python", "python3"));
3900    }
3901
3902    #[test]
3903    fn test_sorting_order_wrap_and_sort_regular_packages() {
3904        let order = WrapAndSortOrder;
3905        // Regular packages sorted alphabetically
3906        assert!(order.lt("aaa", "bbb"));
3907        assert!(order.lt("python3", "rustc"));
3908        assert!(!order.lt("rustc", "python3"));
3909    }
3910
3911    #[test]
3912    fn test_sorting_order_wrap_and_sort_substvars() {
3913        let order = WrapAndSortOrder;
3914        // Substvars should come after regular packages
3915        assert!(order.lt("python3", "${misc:Depends}"));
3916        assert!(!order.lt("${misc:Depends}", "python3"));
3917        // But wrap-and-sort doesn't ignore them
3918        assert!(!order.ignore("${misc:Depends}"));
3919    }
3920
3921    #[test]
3922    fn test_sorting_order_default_special_items() {
3923        let order = DefaultSortingOrder;
3924        // Special items should come after regular items
3925        assert!(order.lt("python3", "${misc:Depends}"));
3926        assert!(order.lt("aaa", "@cdbs@"));
3927        // And should be ignored
3928        assert!(order.ignore("${misc:Depends}"));
3929        assert!(order.ignore("@cdbs@"));
3930        assert!(!order.ignore("python3"));
3931    }
3932
3933    #[test]
3934    fn test_is_special_package_name() {
3935        assert!(is_special_package_name("${misc:Depends}"));
3936        assert!(is_special_package_name("${shlibs:Depends}"));
3937        assert!(is_special_package_name("@cdbs@"));
3938        assert!(!is_special_package_name("python3"));
3939        assert!(!is_special_package_name("debhelper"));
3940    }
3941
3942    #[test]
3943    fn test_add_dependency_with_explicit_position() {
3944        // Test that add_dependency works with explicit position and preserves whitespace
3945        let mut relations: Relations = "python3,  rustc".parse().unwrap();
3946        let entry = Entry::from(Relation::simple("debhelper"));
3947        relations.add_dependency(entry, Some(1));
3948        // Should preserve the 2-space pattern from the original
3949        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc");
3950    }
3951
3952    #[test]
3953    fn test_whitespace_detection_single_space() {
3954        let mut relations: Relations = "python3, rustc".parse().unwrap();
3955        let entry = Entry::from(Relation::simple("debhelper"));
3956        relations.add_dependency(entry, Some(1));
3957        assert_eq!(relations.to_string(), "python3, debhelper, rustc");
3958    }
3959
3960    #[test]
3961    fn test_whitespace_detection_multiple_spaces() {
3962        let mut relations: Relations = "python3,  rustc,  gcc".parse().unwrap();
3963        let entry = Entry::from(Relation::simple("debhelper"));
3964        relations.add_dependency(entry, Some(1));
3965        // Should detect and use the 2-space pattern
3966        assert_eq!(relations.to_string(), "python3,  debhelper,  rustc,  gcc");
3967    }
3968
3969    #[test]
3970    fn test_whitespace_detection_mixed_patterns() {
3971        // When patterns differ, use the most common one
3972        let mut relations: Relations = "a, b, c,  d, e".parse().unwrap();
3973        let entry = Entry::from(Relation::simple("x"));
3974        relations.push(entry);
3975        // Three single-space (after a, b, d), one double-space (after c)
3976        // Should use single space as it's most common
3977        assert_eq!(relations.to_string(), "a, b, c,  d, e, x");
3978    }
3979
3980    #[test]
3981    fn test_whitespace_detection_newlines() {
3982        let mut relations: Relations = "python3,\n rustc".parse().unwrap();
3983        let entry = Entry::from(Relation::simple("debhelper"));
3984        relations.add_dependency(entry, Some(1));
3985        // Detects full pattern including newline
3986        assert_eq!(relations.to_string(), "python3,\n debhelper,\n rustc");
3987    }
3988
3989    #[test]
3990    fn test_append_with_newline_no_trailing() {
3991        let mut relations: Relations = "foo,\n bar".parse().unwrap();
3992        let entry = Entry::from(Relation::simple("blah"));
3993        relations.add_dependency(entry, None);
3994        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
3995    }
3996
3997    #[test]
3998    fn test_append_with_trailing_newline() {
3999        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4000        let entry = Entry::from(Relation::simple("blah"));
4001        relations.add_dependency(entry, None);
4002        assert_eq!(relations.to_string(), "foo,\n bar,\n blah");
4003    }
4004
4005    #[test]
4006    fn test_append_with_4_space_indent() {
4007        let mut relations: Relations = "foo,\n    bar".parse().unwrap();
4008        let entry = Entry::from(Relation::simple("blah"));
4009        relations.add_dependency(entry, None);
4010        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4011    }
4012
4013    #[test]
4014    fn test_append_with_4_space_and_trailing_newline() {
4015        let mut relations: Relations = "foo,\n    bar\n".parse().unwrap();
4016        let entry = Entry::from(Relation::simple("blah"));
4017        relations.add_dependency(entry, None);
4018        assert_eq!(relations.to_string(), "foo,\n    bar,\n    blah");
4019    }
4020
4021    #[test]
4022    fn test_odd_syntax_append_no_trailing() {
4023        let mut relations: Relations = "\n foo\n , bar".parse().unwrap();
4024        let entry = Entry::from(Relation::simple("blah"));
4025        relations.add_dependency(entry, None);
4026        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4027    }
4028
4029    #[test]
4030    fn test_odd_syntax_append_with_trailing() {
4031        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4032        let entry = Entry::from(Relation::simple("blah"));
4033        relations.add_dependency(entry, None);
4034        assert_eq!(relations.to_string(), "\n foo\n , bar\n , blah");
4035    }
4036
4037    #[test]
4038    fn test_insert_at_1_no_trailing() {
4039        let mut relations: Relations = "foo,\n bar".parse().unwrap();
4040        let entry = Entry::from(Relation::simple("blah"));
4041        relations.add_dependency(entry, Some(1));
4042        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4043    }
4044
4045    #[test]
4046    fn test_insert_at_1_with_trailing() {
4047        let mut relations: Relations = "foo,\n bar\n".parse().unwrap();
4048        let entry = Entry::from(Relation::simple("blah"));
4049        relations.add_dependency(entry, Some(1));
4050        assert_eq!(relations.to_string(), "foo,\n blah,\n bar");
4051    }
4052
4053    #[test]
4054    fn test_odd_syntax_insert_at_1() {
4055        let mut relations: Relations = "\n foo\n , bar\n".parse().unwrap();
4056        let entry = Entry::from(Relation::simple("blah"));
4057        relations.add_dependency(entry, Some(1));
4058        assert_eq!(relations.to_string(), "\n foo\n , blah\n , bar");
4059    }
4060
4061    #[test]
4062    fn test_relations_preserves_exact_whitespace() {
4063        // Test that Relations preserves exact whitespace from input
4064        let input =
4065            "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], pkg-config";
4066
4067        let relations: Relations = input.parse().unwrap();
4068
4069        // The whitespace should be preserved in the syntax tree
4070        assert_eq!(
4071            relations.to_string(),
4072            input,
4073            "Relations should preserve exact whitespace from input"
4074        );
4075    }
4076
4077    #[test]
4078    fn test_remove_entry_preserves_indentation() {
4079        // Test that removing an entry preserves the indentation pattern
4080        let input = "debhelper (>= 10), quilt (>= 0.40),\n    libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config";
4081
4082        let mut relations: Relations = input.parse().unwrap();
4083
4084        // Find and remove dh-systemd entry (index 2)
4085        let mut to_remove = Vec::new();
4086        for (idx, entry) in relations.entries().enumerate() {
4087            for relation in entry.relations() {
4088                if relation.name() == "dh-systemd" {
4089                    to_remove.push(idx);
4090                    break;
4091                }
4092            }
4093        }
4094
4095        for idx in to_remove.into_iter().rev() {
4096            relations.remove_entry(idx);
4097        }
4098
4099        let output = relations.to_string();
4100        println!("After removal: '{}'", output);
4101
4102        // The 4-space indentation should be preserved
4103        assert!(
4104            output.contains("\n    libsystemd-dev"),
4105            "Expected 4-space indentation to be preserved, but got:\n'{}'",
4106            output
4107        );
4108    }
4109}