semver/
parse.rs

1use crate::backport::*;
2use crate::error::{ErrorKind, Position};
3use crate::identifier::Identifier;
4use crate::{BuildMetadata, Comparator, Op, Prerelease, Version, VersionReq};
5use core::str::FromStr;
6
7/// Error parsing a SemVer version or version requirement.
8///
9/// # Example
10///
11/// ```
12/// use semver::Version;
13///
14/// fn main() {
15///     let err = Version::parse("1.q.r").unwrap_err();
16///
17///     // "unexpected character 'q' while parsing minor version number"
18///     eprintln!("{}", err);
19/// }
20/// ```
21pub struct Error {
22    pub(crate) kind: ErrorKind,
23}
24
25impl FromStr for Version {
26    type Err = Error;
27
28    fn from_str(text: &str) -> Result<Self, Self::Err> {
29        if text.is_empty() {
30            return Err(Error::new(ErrorKind::Empty));
31        }
32
33        let mut pos = Position::Major;
34        let (major, text) = numeric_identifier(text, pos)?;
35        let text = dot(text, pos)?;
36
37        pos = Position::Minor;
38        let (minor, text) = numeric_identifier(text, pos)?;
39        let text = dot(text, pos)?;
40
41        pos = Position::Patch;
42        let (patch, text) = numeric_identifier(text, pos)?;
43
44        if text.is_empty() {
45            return Ok(Version::new(major, minor, patch));
46        }
47
48        let (pre, text) = if let Some(text) = text.strip_prefix('-') {
49            pos = Position::Pre;
50            let (pre, text) = prerelease_identifier(text)?;
51            if pre.is_empty() {
52                return Err(Error::new(ErrorKind::EmptySegment(pos)));
53            }
54            (pre, text)
55        } else {
56            (Prerelease::EMPTY, text)
57        };
58
59        let (build, text) = if let Some(text) = text.strip_prefix('+') {
60            pos = Position::Build;
61            let (build, text) = build_identifier(text)?;
62            if build.is_empty() {
63                return Err(Error::new(ErrorKind::EmptySegment(pos)));
64            }
65            (build, text)
66        } else {
67            (BuildMetadata::EMPTY, text)
68        };
69
70        if let Some(unexpected) = text.chars().next() {
71            return Err(Error::new(ErrorKind::UnexpectedCharAfter(pos, unexpected)));
72        }
73
74        Ok(Version {
75            major,
76            minor,
77            patch,
78            pre,
79            build,
80        })
81    }
82}
83
84impl FromStr for VersionReq {
85    type Err = Error;
86
87    fn from_str(text: &str) -> Result<Self, Self::Err> {
88        let text = text.trim_start_matches(' ');
89        if let Some((ch, text)) = wildcard(text) {
90            let rest = text.trim_start_matches(' ');
91            if rest.is_empty() {
92                #[cfg(not(no_const_vec_new))]
93                return Ok(VersionReq::STAR);
94                #[cfg(no_const_vec_new)] // rustc <1.39
95                return Ok(VersionReq {
96                    comparators: Vec::new(),
97                });
98            } else if rest.starts_with(',') {
99                return Err(Error::new(ErrorKind::WildcardNotTheOnlyComparator(ch)));
100            } else {
101                return Err(Error::new(ErrorKind::UnexpectedAfterWildcard));
102            }
103        }
104
105        let depth = 0;
106        let mut comparators = Vec::new();
107        let len = version_req(text, &mut comparators, depth)?;
108        unsafe { comparators.set_len(len) }
109        Ok(VersionReq { comparators })
110    }
111}
112
113impl FromStr for Comparator {
114    type Err = Error;
115
116    fn from_str(text: &str) -> Result<Self, Self::Err> {
117        let text = text.trim_start_matches(' ');
118        let (comparator, pos, rest) = comparator(text)?;
119        if !rest.is_empty() {
120            let unexpected = rest.chars().next().unwrap();
121            return Err(Error::new(ErrorKind::UnexpectedCharAfter(pos, unexpected)));
122        }
123        Ok(comparator)
124    }
125}
126
127impl FromStr for Prerelease {
128    type Err = Error;
129
130    fn from_str(text: &str) -> Result<Self, Self::Err> {
131        let (pre, rest) = prerelease_identifier(text)?;
132        if !rest.is_empty() {
133            return Err(Error::new(ErrorKind::IllegalCharacter(Position::Pre)));
134        }
135        Ok(pre)
136    }
137}
138
139impl FromStr for BuildMetadata {
140    type Err = Error;
141
142    fn from_str(text: &str) -> Result<Self, Self::Err> {
143        let (build, rest) = build_identifier(text)?;
144        if !rest.is_empty() {
145            return Err(Error::new(ErrorKind::IllegalCharacter(Position::Build)));
146        }
147        Ok(build)
148    }
149}
150
151impl Error {
152    fn new(kind: ErrorKind) -> Self {
153        Error { kind }
154    }
155}
156
157impl Op {
158    const DEFAULT: Self = Op::Caret;
159}
160
161fn numeric_identifier(input: &str, pos: Position) -> Result<(u64, &str), Error> {
162    let mut len = 0;
163    let mut value = 0u64;
164
165    while let Some(&digit) = input.as_bytes().get(len) {
166        if digit < b'0' || digit > b'9' {
167            break;
168        }
169        if value == 0 && len > 0 {
170            return Err(Error::new(ErrorKind::LeadingZero(pos)));
171        }
172        match value
173            .checked_mul(10)
174            .and_then(|value| value.checked_add((digit - b'0') as u64))
175        {
176            Some(sum) => value = sum,
177            None => return Err(Error::new(ErrorKind::Overflow(pos))),
178        }
179        len += 1;
180    }
181
182    if len > 0 {
183        Ok((value, &input[len..]))
184    } else if let Some(unexpected) = input[len..].chars().next() {
185        Err(Error::new(ErrorKind::UnexpectedChar(pos, unexpected)))
186    } else {
187        Err(Error::new(ErrorKind::UnexpectedEnd(pos)))
188    }
189}
190
191fn wildcard(input: &str) -> Option<(char, &str)> {
192    if let Some(rest) = input.strip_prefix('*') {
193        Some(('*', rest))
194    } else if let Some(rest) = input.strip_prefix('x') {
195        Some(('x', rest))
196    } else if let Some(rest) = input.strip_prefix('X') {
197        Some(('X', rest))
198    } else {
199        None
200    }
201}
202
203fn dot(input: &str, pos: Position) -> Result<&str, Error> {
204    if let Some(rest) = input.strip_prefix('.') {
205        Ok(rest)
206    } else if let Some(unexpected) = input.chars().next() {
207        Err(Error::new(ErrorKind::UnexpectedCharAfter(pos, unexpected)))
208    } else {
209        Err(Error::new(ErrorKind::UnexpectedEnd(pos)))
210    }
211}
212
213fn prerelease_identifier(input: &str) -> Result<(Prerelease, &str), Error> {
214    let (string, rest) = identifier(input, Position::Pre)?;
215    let identifier = unsafe { Identifier::new_unchecked(string) };
216    Ok((Prerelease { identifier }, rest))
217}
218
219fn build_identifier(input: &str) -> Result<(BuildMetadata, &str), Error> {
220    let (string, rest) = identifier(input, Position::Build)?;
221    let identifier = unsafe { Identifier::new_unchecked(string) };
222    Ok((BuildMetadata { identifier }, rest))
223}
224
225fn identifier(input: &str, pos: Position) -> Result<(&str, &str), Error> {
226    let mut accumulated_len = 0;
227    let mut segment_len = 0;
228    let mut segment_has_nondigit = false;
229
230    loop {
231        match input.as_bytes().get(accumulated_len + segment_len) {
232            Some(b'A'..=b'Z') | Some(b'a'..=b'z') | Some(b'-') => {
233                segment_len += 1;
234                segment_has_nondigit = true;
235            }
236            Some(b'0'..=b'9') => {
237                segment_len += 1;
238            }
239            boundary => {
240                if segment_len == 0 {
241                    if accumulated_len == 0 && boundary != Some(&b'.') {
242                        return Ok(("", input));
243                    } else {
244                        return Err(Error::new(ErrorKind::EmptySegment(pos)));
245                    }
246                }
247                if pos == Position::Pre
248                    && segment_len > 1
249                    && !segment_has_nondigit
250                    && input[accumulated_len..].starts_with('0')
251                {
252                    return Err(Error::new(ErrorKind::LeadingZero(pos)));
253                }
254                accumulated_len += segment_len;
255                if boundary == Some(&b'.') {
256                    accumulated_len += 1;
257                    segment_len = 0;
258                    segment_has_nondigit = false;
259                } else {
260                    return Ok(input.split_at(accumulated_len));
261                }
262            }
263        }
264    }
265}
266
267fn op(input: &str) -> (Op, &str) {
268    let bytes = input.as_bytes();
269    if bytes.first() == Some(&b'=') {
270        (Op::Exact, &input[1..])
271    } else if bytes.first() == Some(&b'>') {
272        if bytes.get(1) == Some(&b'=') {
273            (Op::GreaterEq, &input[2..])
274        } else {
275            (Op::Greater, &input[1..])
276        }
277    } else if bytes.first() == Some(&b'<') {
278        if bytes.get(1) == Some(&b'=') {
279            (Op::LessEq, &input[2..])
280        } else {
281            (Op::Less, &input[1..])
282        }
283    } else if bytes.first() == Some(&b'~') {
284        (Op::Tilde, &input[1..])
285    } else if bytes.first() == Some(&b'^') {
286        (Op::Caret, &input[1..])
287    } else {
288        (Op::DEFAULT, input)
289    }
290}
291
292fn comparator(input: &str) -> Result<(Comparator, Position, &str), Error> {
293    let (mut op, text) = op(input);
294    let default_op = input.len() == text.len();
295    let text = text.trim_start_matches(' ');
296
297    let mut pos = Position::Major;
298    let (major, text) = numeric_identifier(text, pos)?;
299    let mut has_wildcard = false;
300
301    let (minor, text) = if let Some(text) = text.strip_prefix('.') {
302        pos = Position::Minor;
303        if let Some((_, text)) = wildcard(text) {
304            has_wildcard = true;
305            if default_op {
306                op = Op::Wildcard;
307            }
308            (None, text)
309        } else {
310            let (minor, text) = numeric_identifier(text, pos)?;
311            (Some(minor), text)
312        }
313    } else {
314        (None, text)
315    };
316
317    let (patch, text) = if let Some(text) = text.strip_prefix('.') {
318        pos = Position::Patch;
319        if let Some((_, text)) = wildcard(text) {
320            if default_op {
321                op = Op::Wildcard;
322            }
323            (None, text)
324        } else if has_wildcard {
325            return Err(Error::new(ErrorKind::UnexpectedAfterWildcard));
326        } else {
327            let (patch, text) = numeric_identifier(text, pos)?;
328            (Some(patch), text)
329        }
330    } else {
331        (None, text)
332    };
333
334    let (pre, text) = if patch.is_some() && text.starts_with('-') {
335        pos = Position::Pre;
336        let text = &text[1..];
337        let (pre, text) = prerelease_identifier(text)?;
338        if pre.is_empty() {
339            return Err(Error::new(ErrorKind::EmptySegment(pos)));
340        }
341        (pre, text)
342    } else {
343        (Prerelease::EMPTY, text)
344    };
345
346    let text = if patch.is_some() && text.starts_with('+') {
347        pos = Position::Build;
348        let text = &text[1..];
349        let (build, text) = build_identifier(text)?;
350        if build.is_empty() {
351            return Err(Error::new(ErrorKind::EmptySegment(pos)));
352        }
353        text
354    } else {
355        text
356    };
357
358    let text = text.trim_start_matches(' ');
359
360    let comparator = Comparator {
361        op,
362        major,
363        minor,
364        patch,
365        pre,
366    };
367
368    Ok((comparator, pos, text))
369}
370
371fn version_req(input: &str, out: &mut Vec<Comparator>, depth: usize) -> Result<usize, Error> {
372    let (comparator, pos, text) = match comparator(input) {
373        Ok(success) => success,
374        Err(mut error) => {
375            if let Some((ch, mut rest)) = wildcard(input) {
376                rest = rest.trim_start_matches(' ');
377                if rest.is_empty() || rest.starts_with(',') {
378                    error.kind = ErrorKind::WildcardNotTheOnlyComparator(ch);
379                }
380            }
381            return Err(error);
382        }
383    };
384
385    if text.is_empty() {
386        out.reserve_exact(depth + 1);
387        unsafe { out.as_mut_ptr().add(depth).write(comparator) }
388        return Ok(depth + 1);
389    }
390
391    let text = if let Some(text) = text.strip_prefix(',') {
392        text.trim_start_matches(' ')
393    } else {
394        let unexpected = text.chars().next().unwrap();
395        return Err(Error::new(ErrorKind::ExpectedCommaFound(pos, unexpected)));
396    };
397
398    const MAX_COMPARATORS: usize = 32;
399    if depth + 1 == MAX_COMPARATORS {
400        return Err(Error::new(ErrorKind::ExcessiveComparators));
401    }
402
403    // Recurse to collect parsed Comparator objects on the stack. We perform a
404    // single allocation to allocate exactly the right sized Vec only once the
405    // total number of comparators is known.
406    let len = version_req(text, out, depth + 1)?;
407    unsafe { out.as_mut_ptr().add(depth).write(comparator) }
408    Ok(len)
409}