debian_control/lossy/
relations.rs

1//! Parser for relationship fields like `Depends`, `Recommends`, etc.
2//!
3//! # Example
4//! ```
5//! use debian_control::lossy::{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(1);
18//! relations[0][0].archqual = Some("amd64".to_string());
19//! assert_eq!(relations.to_string(), "python3-dulwich:amd64 (>= 0.19.0), python3-urllib3 (<< 1.26.0)");
20//! ```
21
22use std::iter::Peekable;
23
24use crate::relations::SyntaxKind::*;
25use crate::relations::{lex, BuildProfile, SyntaxKind, VersionConstraint};
26
27/// A relation entry in a relationship field.
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct Relation {
30    /// Package name.
31    pub name: String,
32    /// Architecture qualifier.
33    pub archqual: Option<String>,
34    /// Architectures that this relation is only valid for.
35    pub architectures: Option<Vec<String>>,
36    /// Version constraint and version.
37    pub version: Option<(VersionConstraint, debversion::Version)>,
38    /// Build profiles that this relation is only valid for.
39    pub profiles: Vec<Vec<BuildProfile>>,
40}
41
42impl Default for Relation {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl Relation {
49    /// Create an empty relation.
50    pub fn new() -> Self {
51        Self {
52            name: String::new(),
53            archqual: None,
54            architectures: None,
55            version: None,
56            profiles: Vec::new(),
57        }
58    }
59
60    /// Build a new relation
61    pub fn build(name: &str) -> RelationBuilder {
62        RelationBuilder::new(name)
63    }
64
65    /// Check if this entry is satisfied by the given package versions.
66    ///
67    /// # Arguments
68    /// * `package_version` - A function that returns the version of a package.
69    ///
70    /// # Example
71    /// ```
72    /// use debian_control::lossy::Relation;
73    /// let entry: Relation = "samba (>= 2.0)".parse().unwrap();
74    /// assert!(entry.satisfied_by(|name: &str| -> Option<debversion::Version> {
75    ///    match name {
76    ///    "samba" => Some("2.0".parse().unwrap()),
77    ///    _ => None
78    /// }}));
79    /// ```
80    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup) -> bool {
81        let actual = package_version.lookup_version(self.name.as_str());
82        if let Some((vc, version)) = &self.version {
83            if let Some(actual) = actual {
84                match vc {
85                    VersionConstraint::GreaterThanEqual => actual.as_ref() >= version,
86                    VersionConstraint::LessThanEqual => actual.as_ref() <= version,
87                    VersionConstraint::Equal => actual.as_ref() == version,
88                    VersionConstraint::GreaterThan => actual.as_ref() > version,
89                    VersionConstraint::LessThan => actual.as_ref() < version,
90                }
91            } else {
92                false
93            }
94        } else {
95            actual.is_some()
96        }
97    }
98}
99
100/// A builder for a relation entry in a relationship field.
101pub struct RelationBuilder {
102    name: String,
103
104    archqual: Option<String>,
105    architectures: Option<Vec<String>>,
106    version: Option<(VersionConstraint, debversion::Version)>,
107    profiles: Vec<Vec<BuildProfile>>,
108}
109
110impl RelationBuilder {
111    /// Create a new relation builder.
112    pub fn new(name: &str) -> Self {
113        Self {
114            name: name.to_string(),
115            archqual: None,
116            architectures: None,
117            version: None,
118            profiles: Vec::new(),
119        }
120    }
121
122    /// Set the architecture qualifier.
123    pub fn archqual(mut self, archqual: &str) -> Self {
124        self.archqual = Some(archqual.to_string());
125        self
126    }
127
128    /// Set the architectures that this relation is only valid for.
129    pub fn architectures(mut self, architectures: Vec<&str>) -> Self {
130        self.architectures = Some(architectures.into_iter().map(|s| s.to_string()).collect());
131        self
132    }
133
134    /// Set the version constraint and version.
135    pub fn version(mut self, constraint: VersionConstraint, version: &str) -> Self {
136        self.version = Some((constraint, version.parse().unwrap()));
137        self
138    }
139
140    /// Add a build profile that this relation is only valid for.
141    pub fn profile(mut self, profile: Vec<BuildProfile>) -> Self {
142        self.profiles.push(profile);
143        self
144    }
145
146    /// Build the relation.
147    pub fn build(self) -> Relation {
148        Relation {
149            name: self.name,
150            archqual: self.archqual,
151            architectures: self.architectures,
152            version: self.version,
153            profiles: self.profiles,
154        }
155    }
156}
157
158impl std::fmt::Display for Relation {
159    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
160        write!(f, "{}", self.name)?;
161        if let Some(archqual) = &self.archqual {
162            write!(f, ":{}", archqual)?;
163        }
164        if let Some((constraint, version)) = &self.version {
165            write!(f, " ({} {})", constraint, version)?;
166        }
167        if let Some(archs) = &self.architectures {
168            write!(f, " [{}]", archs.join(" "))?;
169        }
170        for profile in &self.profiles {
171            write!(f, " <")?;
172            for (i, profile) in profile.iter().enumerate() {
173                if i > 0 {
174                    write!(f, ", ")?;
175                }
176                write!(f, "{}", profile)?;
177            }
178            write!(f, ">")?;
179        }
180        Ok(())
181    }
182}
183
184#[cfg(feature = "serde")]
185impl<'de> serde::Deserialize<'de> for Relation {
186    fn deserialize<D>(deserializer: D) -> Result<Relation, D::Error>
187    where
188        D: serde::Deserializer<'de>,
189    {
190        let s = String::deserialize(deserializer)?;
191        s.parse().map_err(serde::de::Error::custom)
192    }
193}
194
195#[cfg(feature = "serde")]
196impl serde::Serialize for Relation {
197    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198    where
199        S: serde::Serializer,
200    {
201        self.to_string().serialize(serializer)
202    }
203}
204
205/// A collection of relation entries in a relationship field.
206#[derive(Debug, Clone, PartialEq, Eq, Hash)]
207pub struct Relations(pub Vec<Vec<Relation>>);
208
209impl std::ops::Index<usize> for Relations {
210    type Output = Vec<Relation>;
211
212    fn index(&self, index: usize) -> &Self::Output {
213        &self.0[index]
214    }
215}
216
217impl std::ops::IndexMut<usize> for Relations {
218    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
219        &mut self.0[index]
220    }
221}
222
223impl FromIterator<Vec<Relation>> for Relations {
224    fn from_iter<I: IntoIterator<Item = Vec<Relation>>>(iter: I) -> Self {
225        Self(iter.into_iter().collect())
226    }
227}
228
229impl Default for Relations {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235impl Relations {
236    /// Create an empty relations.
237    pub fn new() -> Self {
238        Self(Vec::new())
239    }
240
241    /// Remove an entry from the relations.
242    pub fn remove(&mut self, index: usize) {
243        self.0.remove(index);
244    }
245
246    /// Iterate over the entries in the relations.
247    pub fn iter(&self) -> impl Iterator<Item = Vec<&Relation>> {
248        self.0.iter().map(|entry| entry.iter().collect())
249    }
250
251    /// Number of entries in the relations.
252    pub fn len(&self) -> usize {
253        self.0.len()
254    }
255
256    /// Check if the relations are empty.
257    pub fn is_empty(&self) -> bool {
258        self.0.is_empty()
259    }
260
261    /// Check if the relations are satisfied by the given package versions.
262    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
263        self.0
264            .iter()
265            .all(|e| e.iter().any(|r| r.satisfied_by(package_version)))
266    }
267}
268
269impl FromIterator<Relation> for Relations {
270    fn from_iter<I: IntoIterator<Item = Relation>>(iter: I) -> Self {
271        Self(iter.into_iter().map(|r| vec![r]).collect())
272    }
273}
274
275impl std::fmt::Display for Relations {
276    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
277        for (i, entry) in self.0.iter().enumerate() {
278            if i > 0 {
279                f.write_str(", ")?;
280            }
281            for (j, relation) in entry.iter().enumerate() {
282                if j > 0 {
283                    f.write_str(" | ")?;
284                }
285                write!(f, "{}", relation)?;
286            }
287        }
288        Ok(())
289    }
290}
291
292impl std::str::FromStr for Relation {
293    type Err = String;
294
295    fn from_str(s: &str) -> Result<Self, Self::Err> {
296        let tokens = lex(s);
297        let mut tokens = tokens.into_iter().peekable();
298
299        fn eat_whitespace(tokens: &mut Peekable<impl Iterator<Item = (SyntaxKind, String)>>) {
300            while let Some((WHITESPACE, _)) = tokens.peek() {
301                tokens.next();
302            }
303        }
304
305        let name = match tokens.next() {
306            Some((IDENT, name)) => name,
307            _ => return Err("Expected package name".to_string()),
308        };
309
310        eat_whitespace(&mut tokens);
311
312        let archqual = if let Some((COLON, _)) = tokens.peek() {
313            tokens.next();
314            match tokens.next() {
315                Some((IDENT, s)) => Some(s),
316                _ => return Err("Expected architecture qualifier".to_string()),
317            }
318        } else {
319            None
320        };
321        eat_whitespace(&mut tokens);
322
323        let version = if let Some((L_PARENS, _)) = tokens.peek() {
324            tokens.next();
325            eat_whitespace(&mut tokens);
326            let mut constraint = String::new();
327            while let Some((kind, t)) = tokens.peek() {
328                match kind {
329                    EQUAL | L_ANGLE | R_ANGLE => {
330                        constraint.push_str(t);
331                        tokens.next();
332                    }
333                    _ => break,
334                }
335            }
336            let constraint = constraint.parse()?;
337            eat_whitespace(&mut tokens);
338            // Read IDENT and COLON tokens until we see R_PARENS
339            let mut version_string = String::new();
340            while let Some((kind, s)) = tokens.peek() {
341                match kind {
342                    R_PARENS => break,
343                    IDENT | COLON => version_string.push_str(s),
344                    n => return Err(format!("Unexpected token: {:?}", n)),
345                }
346                tokens.next();
347            }
348            let version = version_string
349                .parse()
350                .map_err(|e: debversion::ParseError| e.to_string())?;
351            eat_whitespace(&mut tokens);
352            if let Some((R_PARENS, _)) = tokens.next() {
353            } else {
354                return Err(format!("Expected ')', found {:?}", tokens.next()));
355            }
356            Some((constraint, version))
357        } else {
358            None
359        };
360
361        eat_whitespace(&mut tokens);
362
363        let architectures = if let Some((L_BRACKET, _)) = tokens.peek() {
364            tokens.next();
365            let mut archs = Vec::new();
366            loop {
367                match tokens.next() {
368                    Some((IDENT, s)) => archs.push(s),
369                    Some((WHITESPACE, _)) => {}
370                    Some((R_BRACKET, _)) => break,
371                    _ => return Err("Expected architecture name".to_string()),
372                }
373            }
374            Some(archs)
375        } else {
376            None
377        };
378
379        eat_whitespace(&mut tokens);
380
381        let mut profiles = Vec::new();
382        while let Some((L_ANGLE, _)) = tokens.peek() {
383            tokens.next();
384            loop {
385                let mut profile = Vec::new();
386                loop {
387                    match tokens.next() {
388                        Some((NOT, _)) => {
389                            let profile_name = match tokens.next() {
390                                Some((IDENT, s)) => s,
391                                _ => return Err("Expected profile name".to_string()),
392                            };
393                            profile.push(BuildProfile::Disabled(profile_name));
394                        }
395                        Some((IDENT, s)) => profile.push(BuildProfile::Enabled(s)),
396                        Some((WHITESPACE, _)) => {}
397                        _ => return Err("Expected profile name".to_string()),
398                    }
399                    if let Some((COMMA, _)) = tokens.peek() {
400                        tokens.next();
401                    } else {
402                        break;
403                    }
404                }
405                profiles.push(profile);
406                if let Some((R_ANGLE, _)) = tokens.next() {
407                    eat_whitespace(&mut tokens);
408                    break;
409                }
410            }
411        }
412
413        eat_whitespace(&mut tokens);
414
415        if let Some((kind, _)) = tokens.next() {
416            return Err(format!("Unexpected token: {:?}", kind));
417        }
418
419        Ok(Relation {
420            name,
421            archqual,
422            architectures,
423            version,
424            profiles,
425        })
426    }
427}
428
429impl std::str::FromStr for Relations {
430    type Err = String;
431
432    fn from_str(s: &str) -> Result<Self, Self::Err> {
433        let mut relations = Vec::new();
434        if s.is_empty() {
435            return Ok(Relations(relations));
436        }
437        for entry in s.split(',') {
438            let entry = entry.trim();
439            if entry.is_empty() {
440                // Ignore empty entries.
441                continue;
442            }
443            let entry_relations = entry.split('|').map(|relation| {
444                let relation = relation.trim();
445                if relation.is_empty() {
446                    return Err("Empty relation".to_string());
447                }
448                relation.parse()
449            });
450            relations.push(entry_relations.collect::<Result<Vec<_>, _>>()?);
451        }
452        Ok(Relations(relations))
453    }
454}
455
456#[cfg(feature = "serde")]
457impl<'de> serde::Deserialize<'de> for Relations {
458    fn deserialize<D>(deserializer: D) -> Result<Relations, D::Error>
459    where
460        D: serde::Deserializer<'de>,
461    {
462        let s = String::deserialize(deserializer)?;
463        s.parse().map_err(serde::de::Error::custom)
464    }
465}
466
467#[cfg(feature = "serde")]
468impl serde::Serialize for Relations {
469    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
470    where
471        S: serde::Serializer,
472    {
473        self.to_string().serialize(serializer)
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn test_parse() {
483        let input = "python3-dulwich";
484        let parsed: Relations = input.parse().unwrap();
485        assert_eq!(parsed.to_string(), input);
486        assert_eq!(parsed.len(), 1);
487        let entry = &parsed[0];
488        assert_eq!(entry.len(), 1);
489        let relation = &entry[0];
490        assert_eq!(relation.to_string(), "python3-dulwich");
491        assert_eq!(relation.version, None);
492
493        let input = "python3-dulwich (>= 0.20.21)";
494        let parsed: Relations = input.parse().unwrap();
495        assert_eq!(parsed.to_string(), input);
496        assert_eq!(parsed.len(), 1);
497        let entry = &parsed[0];
498        assert_eq!(entry.len(), 1);
499        let relation = &entry[0];
500        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
501        assert_eq!(
502            relation.version,
503            Some((
504                VersionConstraint::GreaterThanEqual,
505                "0.20.21".parse().unwrap()
506            ))
507        );
508    }
509
510    #[test]
511    fn test_multiple() {
512        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
513        let parsed: Relations = input.parse().unwrap();
514        assert_eq!(parsed.to_string(), input);
515        assert_eq!(parsed.len(), 2);
516        let entry = &parsed[0];
517        assert_eq!(entry.len(), 1);
518        let relation = &entry[0];
519        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
520        assert_eq!(
521            relation.version,
522            Some((
523                VersionConstraint::GreaterThanEqual,
524                "0.20.21".parse().unwrap()
525            ))
526        );
527        let entry = &parsed[1];
528        assert_eq!(entry.len(), 1);
529        let relation = &entry[0];
530        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
531        assert_eq!(
532            relation.version,
533            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
534        );
535    }
536
537    #[test]
538    fn test_architectures() {
539        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
540        let parsed: Relations = input.parse().unwrap();
541        assert_eq!(parsed.to_string(), input);
542        assert_eq!(parsed.len(), 1);
543        let entry = &parsed[0];
544        assert_eq!(
545            entry[0].to_string(),
546            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
547        );
548        assert_eq!(entry.len(), 1);
549        let relation = &entry[0];
550        assert_eq!(
551            relation.to_string(),
552            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
553        );
554        assert_eq!(relation.version, None);
555        assert_eq!(
556            relation.architectures.as_ref().unwrap(),
557            &vec![
558                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
559            ]
560            .into_iter()
561            .map(|s| s.to_string())
562            .collect::<Vec<_>>()
563        );
564    }
565
566    #[test]
567    fn test_profiles() {
568        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
569        let parsed: Relations = input.parse().unwrap();
570        assert_eq!(parsed.to_string(), input);
571        assert_eq!(parsed.iter().count(), 2);
572        let entry = parsed.iter().next().unwrap();
573        assert_eq!(
574            entry[0].to_string(),
575            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
576        );
577        assert_eq!(entry.len(), 1);
578        let relation = entry[0];
579        assert_eq!(
580            relation.to_string(),
581            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
582        );
583        assert_eq!(
584            relation.version,
585            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
586        );
587        assert_eq!(
588            relation.architectures.as_ref().unwrap(),
589            &["i386", "arm"]
590                .into_iter()
591                .map(|s| s.to_string())
592                .collect::<Vec<_>>()
593        );
594        assert_eq!(
595            relation.profiles,
596            vec![
597                vec![BuildProfile::Disabled("nocheck".to_string())],
598                vec![BuildProfile::Disabled("cross".to_string())]
599            ]
600        );
601    }
602
603    #[cfg(feature = "serde")]
604    #[test]
605    fn test_serde_relations() {
606        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
607        let parsed: Relations = input.parse().unwrap();
608        let serialized = serde_json::to_string(&parsed).unwrap();
609        assert_eq!(
610            serialized,
611            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
612        );
613        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
614        assert_eq!(deserialized, parsed);
615    }
616
617    #[cfg(feature = "serde")]
618    #[test]
619    fn test_serde_relation() {
620        let input = "python3-dulwich (>= 0.20.21)";
621        let parsed: Relation = input.parse().unwrap();
622        let serialized = serde_json::to_string(&parsed).unwrap();
623        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
624        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
625        assert_eq!(deserialized, parsed);
626    }
627
628    #[test]
629    fn test_relations_is_empty() {
630        let input = "python3-dulwich (>= 0.20.21)";
631        let parsed: Relations = input.parse().unwrap();
632        assert!(!parsed.is_empty());
633        let input = "";
634        let parsed: Relations = input.parse().unwrap();
635        assert!(parsed.is_empty());
636    }
637
638    #[test]
639    fn test_relations_len() {
640        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
641        let parsed: Relations = input.parse().unwrap();
642        assert_eq!(parsed.len(), 2);
643    }
644
645    #[test]
646    fn test_relations_remove() {
647        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
648        let mut parsed: Relations = input.parse().unwrap();
649        parsed.remove(1);
650        assert_eq!(parsed.len(), 1);
651        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
652    }
653
654    #[test]
655    fn test_relations_satisfied_by() {
656        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
657        let parsed: Relations = input.parse().unwrap();
658        assert!(
659            parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
660                match name {
661                    "python3-dulwich" => Some("0.20.21".parse().unwrap()),
662                    _ => None,
663                }
664            })
665        );
666        assert!(
667            !parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
668                match name {
669                    "python3-dulwich" => Some("0.21".parse().unwrap()),
670                    _ => None,
671                }
672            })
673        );
674    }
675
676    #[test]
677    fn test_relation_satisfied_by() {
678        let input = "python3-dulwich (>= 0.20.21)";
679        let parsed: Relation = input.parse().unwrap();
680        assert!(
681            parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
682                match name {
683                    "python3-dulwich" => Some("0.20.21".parse().unwrap()),
684                    _ => None,
685                }
686            })
687        );
688        assert!(
689            !parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
690                match name {
691                    "python3-dulwich" => Some("0.20.20".parse().unwrap()),
692                    _ => None,
693                }
694            })
695        );
696    }
697
698    #[test]
699    fn test_relations_from_iter() {
700        let relation1 = Relation::build("python3-dulwich")
701            .version(VersionConstraint::GreaterThanEqual, "0.19.0")
702            .build();
703        let relation2 = Relation::build("python3-requests").build();
704
705        let relations: Relations = vec![relation1, relation2].into_iter().collect();
706
707        assert_eq!(
708            relations.to_string(),
709            "python3-dulwich (>= 0.19.0), python3-requests"
710        );
711    }
712}