1use core::cmp::Ordering;
2use core::fmt;
3use core::str::FromStr;
4
5use crate::error::SemverErrorKind;
6use crate::identifier::{BuildMetadata, PreRelease};
7use crate::number::MAX_SAFE_INTEGER;
8use crate::{MAX_LENGTH, SemverError};
9
10#[derive(Debug, Clone, Eq)]
34pub struct Version {
35 pub major: u64,
37 pub minor: u64,
39 pub patch: u64,
41 pub pre_release: PreRelease,
43 pub build: BuildMetadata,
45}
46
47impl Version {
48 #[must_use]
62 pub fn new(major: u64, minor: u64, patch: u64) -> Self {
63 Self {
64 major,
65 minor,
66 patch,
67 pre_release: PreRelease::default(),
68 build: BuildMetadata::default(),
69 }
70 }
71
72 pub fn parse(s: &str) -> Result<Self, SemverError> {
91 if s.len() <= MAX_LENGTH {
92 if let Some(version) = parse_fixed_core_version(s.as_bytes()) {
93 return Ok(version);
94 }
95 }
96 let ascii_trimmed = trim_ascii_whitespace(s);
97 if ascii_trimmed.len() != s.len() && ascii_trimmed.len() <= MAX_LENGTH {
98 if let Some(version) = parse_fixed_core_version(ascii_trimmed.as_bytes()) {
99 return Ok(version);
100 }
101 }
102 parse_version(s)
103 }
104
105 #[must_use]
122 pub fn cmp_build(&self, other: &Self) -> Ordering {
123 match compare_core_and_prerelease(self, other) {
124 Ordering::Equal => self.build.cmp(&other.build),
125 ord @ (Ordering::Less | Ordering::Greater) => ord,
126 }
127 }
128}
129
130impl PartialEq for Version {
131 fn eq(&self, other: &Self) -> bool {
132 self.major == other.major
133 && self.minor == other.minor
134 && self.patch == other.patch
135 && self.pre_release == other.pre_release
136 }
137}
138
139impl PartialOrd for Version {
140 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
141 Some(self.cmp(other))
142 }
143}
144
145impl Ord for Version {
146 fn cmp(&self, other: &Self) -> Ordering {
147 compare_core_and_prerelease(self, other)
148 }
149}
150
151impl fmt::Display for Version {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
154 if !self.pre_release.is_empty() {
155 write!(f, "-{}", self.pre_release)?;
156 }
157 if !self.build.is_empty() {
158 write!(f, "+{}", self.build)?;
159 }
160 Ok(())
161 }
162}
163
164impl FromStr for Version {
165 type Err = SemverError;
166 fn from_str(s: &str) -> Result<Self, Self::Err> {
167 Self::parse(s)
168 }
169}
170
171fn parse_version(s: &str) -> Result<Version, SemverError> {
176 if s.len() <= MAX_LENGTH {
177 if let Some(version) = parse_fast_version(s) {
178 return Ok(version);
179 }
180 }
181 let ascii_trimmed = trim_ascii_whitespace(s);
182 if ascii_trimmed.len() != s.len() && ascii_trimmed.len() <= MAX_LENGTH {
183 if let Some(version) = parse_fast_version(ascii_trimmed) {
184 return Ok(version);
185 }
186 }
187 let raw = s.trim();
188 if raw.is_empty() {
189 return Err(SemverErrorKind::Empty.into());
190 }
191 if raw.len() > MAX_LENGTH {
192 return Err(SemverErrorKind::MaxLengthExceeded.into());
193 }
194 let b = raw.as_bytes();
195
196 let mut pos = usize::from(matches!(b.first(), Some(b'v')));
198
199 let major = parse_nr_at(b, &mut pos)?;
201 if b.get(pos) != Some(&b'.') {
202 return Err(SemverErrorKind::MissingVersionSegment.into());
203 }
204 pos += 1;
205 let minor = parse_nr_at(b, &mut pos)?;
206 if b.get(pos) != Some(&b'.') {
207 return Err(SemverErrorKind::MissingVersionSegment.into());
208 }
209 pos += 1;
210 let patch = parse_nr_at(b, &mut pos)?;
211
212 let pre_release = if b.get(pos) == Some(&b'-') {
214 pos += 1;
215 let start = pos;
216 while pos < b.len() && b[pos] != b'+' {
217 pos += 1;
218 }
219 let pre_str = &raw[start..pos];
220 if pre_str.is_empty() {
221 return Err(SemverErrorKind::EmptySegment.into());
222 }
223 PreRelease::new(pre_str)?
224 } else {
225 PreRelease::default()
226 };
227
228 let build = if b.get(pos) == Some(&b'+') {
230 pos += 1;
231 BuildMetadata::new(&raw[pos..])?
232 } else if pos == b.len() {
233 BuildMetadata::default()
234 } else {
235 let unexpected = raw[pos..].chars().next().unwrap_or('\0');
236 return Err(SemverErrorKind::UnexpectedCharacter(unexpected).into());
237 };
238
239 Ok(Version {
240 major,
241 minor,
242 patch,
243 pre_release,
244 build,
245 })
246}
247
248fn parse_fixed_core_version(bytes: &[u8]) -> Option<Version> {
249 let pos = usize::from(matches!(bytes.first(), Some(b'v')));
250 if bytes.len() - pos == 5
251 && bytes[pos].is_ascii_digit()
252 && bytes[pos + 1] == b'.'
253 && bytes[pos + 2].is_ascii_digit()
254 && bytes[pos + 3] == b'.'
255 && bytes[pos + 4].is_ascii_digit()
256 {
257 return Some(Version::new(
258 u64::from(bytes[pos] - b'0'),
259 u64::from(bytes[pos + 2] - b'0'),
260 u64::from(bytes[pos + 4] - b'0'),
261 ));
262 }
263
264 None
265}
266
267fn parse_fast_version(input: &str) -> Option<Version> {
268 let bytes = input.as_bytes();
269 let mut pos = usize::from(matches!(bytes.first(), Some(b'v')));
270 let major = parse_simple_core_number(bytes, &mut pos)?;
271 if bytes.get(pos) != Some(&b'.') {
272 return None;
273 }
274 pos += 1;
275
276 let minor = parse_simple_core_number(bytes, &mut pos)?;
277 if bytes.get(pos) != Some(&b'.') {
278 return None;
279 }
280 pos += 1;
281
282 let patch = parse_simple_core_number(bytes, &mut pos)?;
283 if pos == bytes.len() {
284 return Some(Version::new(major, minor, patch));
285 }
286
287 let pre_release = if bytes.get(pos) == Some(&b'-') {
288 pos += 1;
289 let start = pos;
290 while pos < bytes.len() && bytes[pos] != b'+' {
291 pos += 1;
292 }
293 if start == pos {
294 return None;
295 }
296 PreRelease::new(&input[start..pos]).ok()?
297 } else {
298 PreRelease::default()
299 };
300
301 let build = if bytes.get(pos) == Some(&b'+') {
302 BuildMetadata::new(&input[pos + 1..]).ok()?
303 } else if pos == bytes.len() {
304 BuildMetadata::default()
305 } else {
306 return None;
307 };
308
309 Some(Version {
310 major,
311 minor,
312 patch,
313 pre_release,
314 build,
315 })
316}
317
318fn trim_ascii_whitespace(input: &str) -> &str {
319 let bytes = input.as_bytes();
320 let mut start = 0;
321 while bytes.get(start).is_some_and(u8::is_ascii_whitespace) {
322 start += 1;
323 }
324
325 let mut end = bytes.len();
326 while end > start && bytes[end - 1].is_ascii_whitespace() {
327 end -= 1;
328 }
329 &input[start..end]
330}
331
332fn parse_simple_core_number(bytes: &[u8], pos: &mut usize) -> Option<u64> {
333 let start = *pos;
334 let first = bytes.get(start).copied()?;
335 if !first.is_ascii_digit()
336 || (first == b'0' && bytes.get(start + 1).is_some_and(u8::is_ascii_digit))
337 {
338 return None;
339 }
340
341 let mut value = 0u64;
342 while let Some(digit @ b'0'..=b'9') = bytes.get(*pos).copied() {
343 if *pos - start == 16 {
344 return None;
345 }
346 value = value * 10 + u64::from(digit - b'0');
347 *pos += 1;
348 }
349 (value <= MAX_SAFE_INTEGER).then_some(value)
350}
351
352fn parse_nr_at(b: &[u8], pos: &mut usize) -> Result<u64, SemverError> {
354 let start = *pos;
355 if start >= b.len() || !b[start].is_ascii_digit() {
356 return Err(SemverErrorKind::InvalidNumber.into());
357 }
358 if b[start] == b'0' && b.get(start + 1).is_some_and(u8::is_ascii_digit) {
360 return Err(SemverErrorKind::LeadingZero.into());
361 }
362 let mut value = 0u64;
363 while let Some(&digit) = b.get(*pos).filter(|digit| digit.is_ascii_digit()) {
364 if *pos - start == 16 {
365 return Err(SemverErrorKind::MaxSafeIntegerExceeded.into());
366 }
367 value = value * 10 + u64::from(digit - b'0');
368 *pos += 1;
369 }
370 if value > MAX_SAFE_INTEGER {
371 return Err(SemverErrorKind::MaxSafeIntegerExceeded.into());
372 }
373 Ok(value)
374}
375
376pub(crate) fn compare_core_and_prerelease(left: &Version, right: &Version) -> Ordering {
377 macro_rules! cmp_field {
378 ($field:ident) => {
379 match left.$field.cmp(&right.$field) {
380 Ordering::Equal => {}
381 ord @ (Ordering::Less | Ordering::Greater) => return ord,
382 }
383 };
384 }
385 cmp_field!(major);
386 cmp_field!(minor);
387 cmp_field!(patch);
388 match (left.pre_release.is_empty(), right.pre_release.is_empty()) {
389 (true, false) => Ordering::Greater,
390 (false, true) => Ordering::Less,
391 (true, true) => Ordering::Equal,
392 (false, false) => left.pre_release.cmp_identifiers(&right.pre_release),
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::{Version, parse_nr_at};
399 use crate::number::MAX_SAFE_INTEGER;
400
401 #[test]
402 fn parse_nr_at_propagates_core_number_parse_errors() {
403 let bytes = b"9007199254740992";
404 let mut pos = 0;
405 assert!(parse_nr_at(bytes, &mut pos).is_err());
406 }
407
408 #[test]
409 fn parse_nr_at_parses_max_safe_integer() {
410 let bytes = b"9007199254740991";
411 let mut pos = 0;
412 assert_eq!(parse_nr_at(bytes, &mut pos).unwrap(), MAX_SAFE_INTEGER);
413 assert_eq!(pos, bytes.len());
414 }
415
416 #[test]
417 fn parse_covers_trimmed_fast_and_unicode_fallback_paths() {
418 assert_eq!(Version::parse(" 1.2.3 ").unwrap(), Version::new(1, 2, 3));
419 assert_eq!(Version::parse(" 10.2.3 ").unwrap(), Version::new(10, 2, 3));
420 assert_eq!(
421 Version::parse("\u{2003}1.2.3\u{2003}").unwrap(),
422 Version::new(1, 2, 3)
423 );
424 }
425}