Coverage Report

Created: 2026-03-31 07:09

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