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