semver/
parse.rs

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