snix_eval/builtins/
versions.rs

1use std::cmp::Ordering;
2use std::iter::{once, Chain, Once};
3use std::ops::RangeInclusive;
4
5use bstr::{BStr, ByteSlice, B};
6
7/// Version strings can be broken up into Parts.
8/// One Part represents either a string of digits or characters.
9/// '.' and '_' represent deviders between parts and are not included in any part.
10#[derive(PartialEq, Eq, Clone, Debug)]
11pub enum VersionPart<'a> {
12    Word(&'a BStr),
13    Number(&'a BStr),
14}
15
16impl PartialOrd for VersionPart<'_> {
17    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
18        Some(self.cmp(other))
19    }
20}
21
22impl Ord for VersionPart<'_> {
23    fn cmp(&self, other: &Self) -> Ordering {
24        match (self, other) {
25            (VersionPart::Number(s1), VersionPart::Number(s2)) => {
26                // Note: C++ Nix uses `int`, but probably doesn't make a difference
27                // We trust that the splitting was done correctly and parsing will work
28                let n1: u64 = s1.to_str_lossy().parse().unwrap();
29                let n2: u64 = s2.to_str_lossy().parse().unwrap();
30                n1.cmp(&n2)
31            }
32
33            // `pre` looses unless the other part is also a `pre`
34            (VersionPart::Word(x), VersionPart::Word(y)) if *x == B("pre") && *y == B("pre") => {
35                Ordering::Equal
36            }
37            (VersionPart::Word(x), _) if *x == B("pre") => Ordering::Less,
38            (_, VersionPart::Word(y)) if *y == B("pre") => Ordering::Greater,
39
40            // Number wins against Word
41            (VersionPart::Number(_), VersionPart::Word(_)) => Ordering::Greater,
42            (VersionPart::Word(_), VersionPart::Number(_)) => Ordering::Less,
43
44            (VersionPart::Word(w1), VersionPart::Word(w2)) => w1.cmp(w2),
45        }
46    }
47}
48
49/// Type used to hold information about a VersionPart during creation
50enum InternalPart {
51    Number { range: RangeInclusive<usize> },
52    Word { range: RangeInclusive<usize> },
53    Break,
54}
55
56/// An iterator which yields the parts of a version string.
57///
58/// This can then be directly used to compare two versions
59pub struct VersionPartsIter<'a> {
60    cached_part: InternalPart,
61    iter: bstr::CharIndices<'a>,
62    version: &'a BStr,
63}
64
65impl<'a> VersionPartsIter<'a> {
66    pub fn new(version: &'a BStr) -> Self {
67        Self {
68            cached_part: InternalPart::Break,
69            iter: version.char_indices(),
70            version,
71        }
72    }
73
74    /// Create an iterator that yields all version parts followed by an additional
75    /// `VersionPart::Word("")` part (i.e. you can think of this as
76    /// `builtins.splitVersion version ++ [ "" ]`). This is necessary, because
77    /// Nix's `compareVersions` is not entirely lexicographical: If we have two
78    /// equal versions, but one is longer, the longer one is only considered
79    /// greater if the first additional part of the longer version is not `pre`,
80    /// e.g. `2.3 > 2.3pre`. It is otherwise lexicographical, so peculiar behavior
81    /// like `2.3 < 2.3.0pre` ensues. Luckily for us, this means that we can
82    /// lexicographically compare two version strings, _if_ we append an extra
83    /// component to both versions.
84    pub fn new_for_cmp(version: &'a BStr) -> Chain<Self, Once<VersionPart<'a>>> {
85        Self::new(version).chain(once(VersionPart::Word("".into())))
86    }
87}
88
89impl<'a> Iterator for VersionPartsIter<'a> {
90    type Item = VersionPart<'a>;
91
92    fn next(&mut self) -> Option<Self::Item> {
93        let char = self.iter.next();
94
95        if char.is_none() {
96            let cached_part = std::mem::replace(&mut self.cached_part, InternalPart::Break);
97            match cached_part {
98                InternalPart::Break => return None,
99                InternalPart::Number { range } => {
100                    return Some(VersionPart::Number(&self.version[range]))
101                }
102                InternalPart::Word { range } => {
103                    return Some(VersionPart::Word(&self.version[range]))
104                }
105            }
106        }
107
108        let (start, end, char) = char.unwrap();
109        match char {
110            // Divider encountered
111            '.' | '-' => {
112                let cached_part = std::mem::replace(&mut self.cached_part, InternalPart::Break);
113                match cached_part {
114                    InternalPart::Number { range } => {
115                        Some(VersionPart::Number(&self.version[range]))
116                    }
117                    InternalPart::Word { range } => Some(VersionPart::Word(&self.version[range])),
118                    InternalPart::Break => self.next(),
119                }
120            }
121
122            // digit encountered
123            _ if char.is_ascii_digit() => {
124                let cached_part = std::mem::replace(
125                    &mut self.cached_part,
126                    InternalPart::Number {
127                        range: start..=(end - 1),
128                    },
129                );
130                match cached_part {
131                    InternalPart::Number { range } => {
132                        self.cached_part = InternalPart::Number {
133                            range: *range.start()..=*range.end() + 1,
134                        };
135                        self.next()
136                    }
137                    InternalPart::Word { range } => Some(VersionPart::Word(&self.version[range])),
138                    InternalPart::Break => self.next(),
139                }
140            }
141
142            // char encountered
143            _ => {
144                let mut cached_part = InternalPart::Word {
145                    range: start..=(end - 1),
146                };
147                std::mem::swap(&mut cached_part, &mut self.cached_part);
148                match cached_part {
149                    InternalPart::Word { range } => {
150                        self.cached_part = InternalPart::Word {
151                            range: *range.start()..=*range.end() + char.len_utf8(),
152                        };
153                        self.next()
154                    }
155                    InternalPart::Number { range } => {
156                        Some(VersionPart::Number(&self.version[range]))
157                    }
158                    InternalPart::Break => self.next(),
159                }
160            }
161        }
162    }
163}