diff --git a/benches/parse.rs b/benches/parse.rs index 13d912c..7af6048 100644 --- a/benches/parse.rs +++ b/benches/parse.rs @@ -8,10 +8,25 @@ use criterion2::{Criterion, Throughput, black_box, criterion_group, criterion_ma use js_semver::{Range, Version}; fn bench_version_parse(c: &mut Criterion) { - let version = "4.5.3"; - let mut group = c.benchmark_group("version_parse"); + bench_version_case(c, "version_parse", "4.5.3"); + bench_version_case(c, "version_parse_prefixed", "v4.5.3"); + bench_version_case(c, "version_parse_whitespace", " 4.5.3 "); + bench_version_case(c, "version_parse_prerelease", "4.1.0-rc.1"); + bench_version_case(c, "version_parse_build", "4.1.0+build.42"); + bench_version_case(c, "version_parse_prerelease_build", "4.1.0-rc.1+build.42"); + bench_version_case( + c, + "version_parse_long_metadata", + "19.3.0-canary-044d56f3-20260330+sha.abcdef0123456789", + ); + bench_version_case(c, "version_parse_invalid_core", "04.1.0"); + bench_version_case(c, "version_parse_invalid_metadata", "4.1.0-alpha..1"); +} + +fn bench_version_case(c: &mut Criterion, name: &str, version: &str) { + let mut group = c.benchmark_group(name); group.throughput(Throughput::Bytes(u64::try_from(version.len()).unwrap_or(0))); - group.bench_function("version", |b| { + group.bench_function("js-semver", |b| { b.iter(|| { if let Ok(parsed) = Version::parse(black_box(version)) { black_box(parsed); @@ -25,7 +40,7 @@ fn bench_range_parse(c: &mut Criterion) { let range = "^4.2.0"; let mut group = c.benchmark_group("range_parse"); group.throughput(Throughput::Bytes(u64::try_from(range.len()).unwrap_or(0))); - group.bench_function("range", |b| { + group.bench_function("js-semver", |b| { b.iter(|| { if let Ok(parsed) = Range::parse(black_box(range)) { black_box(parsed); @@ -42,7 +57,7 @@ fn bench_parse_and_satisfies(c: &mut Criterion) { let bytes = u64::try_from(range.len() + version.len()).unwrap_or(0); group.throughput(Throughput::Bytes(bytes)); - group.bench_function("full", |b| { + group.bench_function("js-semver", |b| { b.iter(|| { if let (Ok(parsed_range), Ok(parsed_version)) = ( Range::parse(black_box(range)), diff --git a/src/identifier.rs b/src/identifier.rs index 2d1f7cc..000a26a 100644 --- a/src/identifier.rs +++ b/src/identifier.rs @@ -36,7 +36,7 @@ impl PreRelease { if s.is_empty() { return Err(SemverErrorKind::Empty.into()); } - validate_identifiers(s, parse_prerelease_identifier)?; + validate_prerelease(s)?; Ok(Self(Box::from(s))) } @@ -114,7 +114,7 @@ impl BuildMetadata { if s.is_empty() { return Err(SemverErrorKind::Empty.into()); } - validate_identifiers(s, parse_build_metadata_identifier)?; + validate_build_metadata(s)?; Ok(Self(Box::from(s))) } @@ -182,54 +182,27 @@ impl<'a> Identifier<'a> { } } -fn parse_prerelease_identifier(raw: &str) -> Result, SemverError> { +fn parse_prerelease_identifier(raw: &str) -> Identifier<'_> { let bytes = raw.as_bytes(); - if bytes.is_empty() { - return Err(SemverErrorKind::EmptySegment.into()); - } - - let mut all_digits = true; - for &byte in bytes { - if byte.is_ascii_digit() { - } else if byte.is_ascii_alphabetic() || byte == b'-' { - all_digits = false; - } else { - return Err(SemverErrorKind::UnexpectedCharacter(char::from(byte)).into()); - } - } - - if all_digits && bytes.len() > 1 && bytes[0] == b'0' { - return Err(SemverErrorKind::LeadingZero.into()); - } - - Ok(Identifier::new( + Identifier::new( raw, - if all_digits { + if bytes.iter().all(u8::is_ascii_digit) { IdentifierKind::Numeric } else { IdentifierKind::AlphaNumeric }, - )) + ) } -fn parse_build_metadata_identifier(raw: &str) -> Result, SemverError> { - if raw.is_empty() { - return Err(SemverErrorKind::EmptySegment.into()); - } - if let Some(byte) = raw - .bytes() - .find(|byte| !byte.is_ascii_alphanumeric() && *byte != b'-') - { - return Err(SemverErrorKind::UnexpectedCharacter(char::from(byte)).into()); - } - Ok(Identifier::new( +fn parse_build_metadata_identifier(raw: &str) -> Identifier<'_> { + Identifier::new( raw, if raw.bytes().all(|byte| byte.is_ascii_digit()) { IdentifierKind::Numeric } else { IdentifierKind::AlphaNumeric }, - )) + ) } impl PartialOrd for Identifier<'_> { @@ -251,18 +224,65 @@ impl Ord for Identifier<'_> { } } -fn validate_identifiers<'a>( - s: &'a str, - parser: fn(&'a str) -> Result, SemverError>, +fn validate_prerelease(s: &str) -> Result<(), SemverError> { + let bytes = s.as_bytes(); + let mut segment_start = 0; + let mut all_digits = true; + + for (pos, &byte) in bytes.iter().enumerate() { + match byte { + b'.' => { + validate_prerelease_segment(bytes, segment_start, pos, all_digits)?; + segment_start = pos + 1; + all_digits = true; + } + b'0'..=b'9' => {} + b'A'..=b'Z' | b'a'..=b'z' | b'-' => all_digits = false, + _ => { + return Err(SemverErrorKind::UnexpectedCharacter(char::from(byte)).into()); + } + } + } + + validate_prerelease_segment(bytes, segment_start, bytes.len(), all_digits) +} + +fn validate_prerelease_segment( + bytes: &[u8], + start: usize, + end: usize, + all_digits: bool, ) -> Result<(), SemverError> { - let mut start = 0; - while start <= s.len() { - let end = next_separator(s, start); - parser(&s[start..end])?; - if end == s.len() { - break; + if start == end { + return Err(SemverErrorKind::EmptySegment.into()); + } + if all_digits && end - start > 1 && bytes[start] == b'0' { + return Err(SemverErrorKind::LeadingZero.into()); + } + Ok(()) +} + +fn validate_build_metadata(s: &str) -> Result<(), SemverError> { + let bytes = s.as_bytes(); + let mut segment_start = 0; + + for (pos, &byte) in bytes.iter().enumerate() { + match byte { + b'.' => { + if pos == segment_start { + return Err(SemverErrorKind::EmptySegment.into()); + } + segment_start = pos + 1; + } + b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' => {} + _ => { + return Err(SemverErrorKind::UnexpectedCharacter(char::from(byte)).into()); + } } - start = end + 1; + } + + if segment_start == bytes.len() { + return Err(SemverErrorKind::EmptySegment.into()); } Ok(()) } @@ -270,7 +290,7 @@ fn validate_identifiers<'a>( fn cmp_dot_separated<'a>( left: &'a str, right: &'a str, - parser: fn(&'a str) -> Result, SemverError>, + parser: fn(&'a str) -> Identifier<'a>, ) -> Ordering { let mut left_start = 0; let mut right_start = 0; @@ -279,25 +299,24 @@ fn cmp_dot_separated<'a>( let right_end = next_separator(right, right_start); let left_part = &left[left_start..left_end]; let right_part = &right[right_start..right_end]; - let Ok(left_id) = parser(left_part) else { - return Ordering::Equal; - }; - let Ok(right_id) = parser(right_part) else { - return Ordering::Equal; - }; + let left_id = parser(left_part); + let right_id = parser(right_part); match left_id.cmp(&right_id) { Ordering::Equal => {} ord @ (Ordering::Less | Ordering::Greater) => return ord, } let left_done = left_end == left.len(); let right_done = right_end == right.len(); - if left_done || right_done { - return match (left_done, right_done) { - (true, false) => Ordering::Less, - (false, true) => Ordering::Greater, - (true, true) | (false, false) => Ordering::Equal, + if left_done { + return if right_done { + Ordering::Equal + } else { + Ordering::Less }; } + if right_done { + return Ordering::Greater; + } left_start = left_end + 1; right_start = right_end + 1; } @@ -326,61 +345,39 @@ mod tests { use super::*; - fn fail_on_bad_left(raw: &str) -> Result, SemverError> { - if raw == "bad-left" { - return Err(SemverErrorKind::Empty.into()); - } - parse_prerelease_identifier(raw) - } - - fn fail_on_bad_right(raw: &str) -> Result, SemverError> { - if raw == "bad-right" { - return Err(SemverErrorKind::Empty.into()); - } - parse_prerelease_identifier(raw) - } - #[test] fn identifier_ordering() { assert_eq!( - parse_build_metadata_identifier("1") - .unwrap() - .cmp(&parse_build_metadata_identifier("2").unwrap()), + parse_build_metadata_identifier("1").cmp(&parse_build_metadata_identifier("2")), Ordering::Less ); assert_eq!( parse_build_metadata_identifier("18446744073709551615") - .unwrap() - .cmp(&parse_build_metadata_identifier("18446744073709551616").unwrap()), + .cmp(&parse_build_metadata_identifier("18446744073709551616")), Ordering::Less ); assert_eq!( parse_build_metadata_identifier("18446744073709551616") - .unwrap() - .cmp(&parse_build_metadata_identifier("18446744073709551617").unwrap()), + .cmp(&parse_build_metadata_identifier("18446744073709551617")), Ordering::Less ); assert_eq!( - parse_prerelease_identifier("1") - .unwrap() - .cmp(&parse_prerelease_identifier("alpha").unwrap()), + parse_prerelease_identifier("1").cmp(&parse_prerelease_identifier("alpha")), Ordering::Less ); assert_eq!( - parse_prerelease_identifier("beta") - .unwrap() - .cmp(&parse_prerelease_identifier("1").unwrap()), + parse_prerelease_identifier("beta").cmp(&parse_prerelease_identifier("1")), Ordering::Greater ); } #[test] fn prerelease_identifier_validation() { - assert!(parse_prerelease_identifier("").is_err()); - assert!(parse_prerelease_identifier("01").is_err()); - assert!(parse_prerelease_identifier("a!b").is_err()); + assert!(PreRelease::new("").is_err()); + assert!(PreRelease::new("01").is_err()); + assert!(PreRelease::new("a!b").is_err()); assert_eq!( - parse_prerelease_identifier("alpha-1").unwrap().kind, + parse_prerelease_identifier("alpha-1").kind, IdentifierKind::AlphaNumeric ); } @@ -452,9 +449,7 @@ mod tests { #[test] fn identifier_partial_cmp_and_prefix_order() { assert_eq!( - parse_prerelease_identifier("alpha") - .unwrap() - .partial_cmp(&parse_prerelease_identifier("alpha").unwrap()), + parse_prerelease_identifier("alpha").partial_cmp(&parse_prerelease_identifier("alpha")), Some(Ordering::Equal) ); assert_eq!( @@ -476,9 +471,7 @@ mod tests { Some(Ordering::Less) ); assert_eq!( - parse_build_metadata_identifier("1") - .unwrap() - .partial_cmp(&parse_build_metadata_identifier("2").unwrap()), + parse_build_metadata_identifier("1").partial_cmp(&parse_build_metadata_identifier("2")), Some(Ordering::Less) ); assert_eq!("rc.1".parse::().unwrap().to_string(), "rc.1"); @@ -488,26 +481,6 @@ mod tests { ); } - #[test] - fn cmp_dot_separated_parser_failures_fallback_to_equal() { - assert_eq!( - fail_on_bad_left("alpha").unwrap(), - parse_prerelease_identifier("alpha").unwrap() - ); - assert_eq!( - fail_on_bad_right("alpha").unwrap(), - parse_prerelease_identifier("alpha").unwrap() - ); - assert_eq!( - cmp_dot_separated("bad-left", "alpha", fail_on_bad_left), - Ordering::Equal - ); - assert_eq!( - cmp_dot_separated("alpha", "bad-right", fail_on_bad_right), - Ordering::Equal - ); - } - #[test] fn prerelease_cmp_identifiers_covers_empty_and_equal_cases() { assert_eq!( diff --git a/src/number.rs b/src/number.rs index 1e10234..7430328 100644 --- a/src/number.rs +++ b/src/number.rs @@ -4,18 +4,6 @@ use crate::error::SemverErrorKind; /// JavaScript's `Number.MAX_SAFE_INTEGER` (2^53 − 1). pub(crate) const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; -pub(crate) fn parse_ascii_digits(digits: &[u8]) -> u64 { - let mut value = 0u64; - for &digit in digits { - debug_assert!( - digit.is_ascii_digit(), - "parse_ascii_digits expects only ASCII digits" - ); - value = value * 10 + u64::from(digit - b'0'); - } - value -} - pub(crate) fn parse_nr(s: &str) -> Result { let b = s.as_bytes(); if b.is_empty() { @@ -54,16 +42,4 @@ mod tests { assert!(parse_nr("9007199254740992").is_err()); assert!(parse_nr("12345678901234567").is_err()); } - - #[test] - fn parse_ascii_digits_parses_valid_input() { - assert_eq!(parse_ascii_digits(b"123"), 123); - } - - #[cfg(all(feature = "std", debug_assertions))] - #[test] - fn parse_ascii_digits_panics_on_non_digits_in_debug_builds() { - let result = std::panic::catch_unwind(|| parse_ascii_digits(b"1a")); - assert!(result.is_err()); - } } diff --git a/src/range.rs b/src/range.rs index 56d225f..13bc9f7 100644 --- a/src/range.rs +++ b/src/range.rs @@ -127,7 +127,30 @@ impl ComparatorSet { /// ``` #[derive(Debug, Clone)] pub struct Range { - set: Vec, + set: ComparatorSets, +} + +#[derive(Debug, Clone)] +enum ComparatorSets { + One(ComparatorSet), + Many(Vec), +} + +impl ComparatorSets { + fn iter(&self) -> core::slice::Iter<'_, ComparatorSet> { + match self { + Self::One(set) => core::slice::from_ref(set).iter(), + Self::Many(sets) => sets.iter(), + } + } + + #[cfg(test)] + fn len(&self) -> usize { + match self { + Self::One(_) => 1, + Self::Many(sets) => sets.len(), + } + } } impl Range { @@ -169,7 +192,7 @@ impl Range { /// ``` #[must_use] pub fn satisfies(&self, version: &Version) -> bool { - for comparator_set in &self.set { + for comparator_set in self.set.iter() { if comparator_set.test(version) { return true; } @@ -240,6 +263,9 @@ fn parse_partial(s: &str) -> Result { if s.is_empty() || s.starts_with('.') { return Err(SemverErrorKind::MissingVersionSegment.into()); } + if let Some(partial) = parse_simple_partial(s) { + return Ok(partial); + } let bytes = s.as_bytes(); let mut core_end = bytes.len(); let mut pre_start = None; @@ -306,12 +332,8 @@ fn parse_partial(s: &str) -> Result { None => PreRelease::default(), }; - match build_part { - Some("") => return Err(SemverErrorKind::EmptySegment.into()), - Some(build) => { - BuildMetadata::new(build)?; - } - None => {} + if let Some(build) = build_part { + BuildMetadata::new(build)?; } Ok(Partial { @@ -322,6 +344,61 @@ fn parse_partial(s: &str) -> Result { }) } +fn parse_simple_partial(s: &str) -> Option { + let bytes = s.as_bytes(); + let (major, mut pos) = parse_simple_component(bytes, 0)?; + let mut minor = None; + let mut patch = None; + + if pos < bytes.len() { + if bytes[pos] != b'.' { + return None; + } + (minor, pos) = parse_simple_component(bytes, pos + 1)?; + } + + if pos < bytes.len() { + if bytes[pos] != b'.' { + return None; + } + (patch, pos) = parse_simple_component(bytes, pos + 1)?; + } + + if pos != bytes.len() { + return None; + } + + Some(Partial { + major, + minor, + patch, + pre_release: PreRelease::default(), + }) +} + +fn parse_simple_component(bytes: &[u8], start: usize) -> Option<(Option, usize)> { + match bytes.get(start).copied()? { + b'x' | b'X' | b'*' => Some((None, start + 1)), + first @ b'0'..=b'9' => { + if first == b'0' && bytes.get(start + 1).is_some_and(u8::is_ascii_digit) { + return None; + } + + let mut pos = start; + let mut value = 0u64; + while let Some(digit @ b'0'..=b'9') = bytes.get(pos).copied() { + if pos - start == 16 { + return None; + } + value = value * 10 + u64::from(digit - b'0'); + pos += 1; + } + (value <= MAX_SAFE_INTEGER).then_some((Some(value), pos)) + } + _ => None, + } +} + fn parse_xr(s: &str) -> Result, SemverError> { match s { "" | "*" | "x" | "X" => Ok(None), @@ -740,7 +817,21 @@ fn parse_range(s: &str) -> Result { let trimmed_prefix_len = s.trim_start_matches(['v', '=', '^', '~', '>', '<']).len(); let bytes = s.as_bytes(); - let mut set = Vec::with_capacity(count_or_groups(bytes)); + let group_count = count_or_groups(bytes); + if group_count == 1 { + let comparator_set = parse_comparator_set(s)?; + if !comparator_set.comparators.is_empty() + && exceeds_max_length + && trimmed_prefix_len > MAX_LENGTH + { + return Err(SemverErrorKind::MaxLengthExceeded.into()); + } + return Ok(Range { + set: ComparatorSets::One(comparator_set), + }); + } + + let mut set = Vec::with_capacity(group_count); let mut start = 0; let mut i = 0; while i < bytes.len() { @@ -765,9 +856,9 @@ fn parse_range(s: &str) -> Result { if has_unbounded_set { return Ok(Range { - set: vec![ComparatorSet { + set: ComparatorSets::One(ComparatorSet { comparators: vec![], - }], + }), }); } @@ -777,7 +868,16 @@ fn parse_range(s: &str) -> Result { set.dedup(); - Ok(Range { set }) + if set.len() == 1 { + let comparator_set = set.remove(0); + return Ok(Range { + set: ComparatorSets::One(comparator_set), + }); + } + + Ok(Range { + set: ComparatorSets::Many(set), + }) } fn parse_comparator_set(s: &str) -> Result { @@ -787,11 +887,17 @@ fn parse_comparator_set(s: &str) -> Result { }); } + let bytes = s.as_bytes(); + if !bytes.iter().any(u8::is_ascii_whitespace) { + let mut comparators = Vec::with_capacity(2); + parse_token_into(&mut comparators, s)?; + return Ok(ComparatorSet { comparators }); + } + if let Some(comps) = try_hyphen(s)? { return Ok(ComparatorSet { comparators: comps }); } - let bytes = s.as_bytes(); let mut all = Vec::with_capacity(count_whitespace_tokens(bytes).saturating_mul(2)); let mut pos = 0; while let Some(t) = next_whitespace_token(s, bytes, &mut pos) { @@ -1295,10 +1401,17 @@ mod tests { assert!(parse_partial("1.bad").is_err()); assert!(parse_partial("1.2-rc.0").is_err()); assert!(parse_partial("2.x-rc.0").is_err()); + assert!(parse_partial("1.2.3+").is_err()); + assert!(parse_partial("10000000000000000").is_err()); + assert_eq!(parse_range("^1.0.0").unwrap().set.len(), 1); assert_eq!(parse_range("1.0.0 || 2.0.0").unwrap().set.len(), 2); assert_eq!(parse_range("1.0.0 || 2.0.0 || 3.0.0").unwrap().set.len(), 3); assert!(parse_range(">= || 1.0.0").is_err()); assert!(parse_range("1.0.0 || >=").is_err()); + assert!(parse_comparator_set(">= ").is_err()); + let mut long_bounded_range = "1.0.0 || ".repeat(29); + long_bounded_range.push_str("1.0.0"); + assert!(parse_range(&long_bounded_range).is_err()); assert_eq!(try_hyphen("1.0.0 - 2.0.0").unwrap().unwrap().len(), 2); assert!(try_hyphen("1.0.0 - 9007199254740991").is_err()); } diff --git a/src/version.rs b/src/version.rs index 4a8e25a..e354c63 100644 --- a/src/version.rs +++ b/src/version.rs @@ -4,7 +4,7 @@ use core::str::FromStr; use crate::error::SemverErrorKind; use crate::identifier::{BuildMetadata, PreRelease}; -use crate::number::{MAX_SAFE_INTEGER, parse_ascii_digits}; +use crate::number::MAX_SAFE_INTEGER; use crate::{MAX_LENGTH, SemverError}; // -------------------------------------------------------------------------- @@ -88,6 +88,17 @@ impl Version { /// /// Returns [`SemverError`] if `s` is not a valid semver string. pub fn parse(s: &str) -> Result { + if s.len() <= MAX_LENGTH { + if let Some(version) = parse_fixed_core_version(s.as_bytes()) { + return Ok(version); + } + } + let ascii_trimmed = trim_ascii_whitespace(s); + if ascii_trimmed.len() != s.len() && ascii_trimmed.len() <= MAX_LENGTH { + if let Some(version) = parse_fixed_core_version(ascii_trimmed.as_bytes()) { + return Ok(version); + } + } parse_version(s) } @@ -153,7 +164,7 @@ impl fmt::Display for Version { impl FromStr for Version { type Err = SemverError; fn from_str(s: &str) -> Result { - parse_version(s) + Self::parse(s) } } @@ -162,6 +173,17 @@ impl FromStr for Version { // -------------------------------------------------------------------------- fn parse_version(s: &str) -> Result { + if s.len() <= MAX_LENGTH { + if let Some(version) = parse_fast_version(s) { + return Ok(version); + } + } + let ascii_trimmed = trim_ascii_whitespace(s); + if ascii_trimmed.len() != s.len() && ascii_trimmed.len() <= MAX_LENGTH { + if let Some(version) = parse_fast_version(ascii_trimmed) { + return Ok(version); + } + } let raw = s.trim(); if raw.is_empty() { return Err(SemverErrorKind::Empty.into()); @@ -223,6 +245,110 @@ fn parse_version(s: &str) -> Result { }) } +fn parse_fixed_core_version(bytes: &[u8]) -> Option { + let pos = usize::from(matches!(bytes.first(), Some(b'v'))); + if bytes.len() - pos == 5 + && bytes[pos].is_ascii_digit() + && bytes[pos + 1] == b'.' + && bytes[pos + 2].is_ascii_digit() + && bytes[pos + 3] == b'.' + && bytes[pos + 4].is_ascii_digit() + { + return Some(Version::new( + u64::from(bytes[pos] - b'0'), + u64::from(bytes[pos + 2] - b'0'), + u64::from(bytes[pos + 4] - b'0'), + )); + } + + None +} + +fn parse_fast_version(input: &str) -> Option { + let bytes = input.as_bytes(); + let mut pos = usize::from(matches!(bytes.first(), Some(b'v'))); + let major = parse_simple_core_number(bytes, &mut pos)?; + if bytes.get(pos) != Some(&b'.') { + return None; + } + pos += 1; + + let minor = parse_simple_core_number(bytes, &mut pos)?; + if bytes.get(pos) != Some(&b'.') { + return None; + } + pos += 1; + + let patch = parse_simple_core_number(bytes, &mut pos)?; + if pos == bytes.len() { + return Some(Version::new(major, minor, patch)); + } + + let pre_release = if bytes.get(pos) == Some(&b'-') { + pos += 1; + let start = pos; + while pos < bytes.len() && bytes[pos] != b'+' { + pos += 1; + } + if start == pos { + return None; + } + PreRelease::new(&input[start..pos]).ok()? + } else { + PreRelease::default() + }; + + let build = if bytes.get(pos) == Some(&b'+') { + BuildMetadata::new(&input[pos + 1..]).ok()? + } else if pos == bytes.len() { + BuildMetadata::default() + } else { + return None; + }; + + Some(Version { + major, + minor, + patch, + pre_release, + build, + }) +} + +fn trim_ascii_whitespace(input: &str) -> &str { + let bytes = input.as_bytes(); + let mut start = 0; + while bytes.get(start).is_some_and(u8::is_ascii_whitespace) { + start += 1; + } + + let mut end = bytes.len(); + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + &input[start..end] +} + +fn parse_simple_core_number(bytes: &[u8], pos: &mut usize) -> Option { + let start = *pos; + let first = bytes.get(start).copied()?; + if !first.is_ascii_digit() + || (first == b'0' && bytes.get(start + 1).is_some_and(u8::is_ascii_digit)) + { + return None; + } + + let mut value = 0u64; + while let Some(digit @ b'0'..=b'9') = bytes.get(*pos).copied() { + if *pos - start == 16 { + return None; + } + value = value * 10 + u64::from(digit - b'0'); + *pos += 1; + } + (value <= MAX_SAFE_INTEGER).then_some(value) +} + /// Parse a decimal integer from `b` starting at `*pos`, advancing `*pos` past the digits. fn parse_nr_at(b: &[u8], pos: &mut usize) -> Result { let start = *pos; @@ -233,18 +359,18 @@ fn parse_nr_at(b: &[u8], pos: &mut usize) -> Result { if b[start] == b'0' && b.get(start + 1).is_some_and(u8::is_ascii_digit) { return Err(SemverErrorKind::LeadingZero.into()); } - while *pos < b.len() && b[*pos].is_ascii_digit() { + let mut value = 0u64; + while let Some(&digit) = b.get(*pos).filter(|digit| digit.is_ascii_digit()) { + if *pos - start == 16 { + return Err(SemverErrorKind::MaxSafeIntegerExceeded.into()); + } + value = value * 10 + u64::from(digit - b'0'); *pos += 1; } - let digits = &b[start..*pos]; - if digits.len() > 16 { - return Err(SemverErrorKind::MaxSafeIntegerExceeded.into()); - } - let n = parse_ascii_digits(digits); - if n > MAX_SAFE_INTEGER { + if value > MAX_SAFE_INTEGER { return Err(SemverErrorKind::MaxSafeIntegerExceeded.into()); } - Ok(n) + Ok(value) } pub(crate) fn compare_core_and_prerelease(left: &Version, right: &Version) -> Ordering { @@ -269,7 +395,7 @@ pub(crate) fn compare_core_and_prerelease(left: &Version, right: &Version) -> Or #[cfg(test)] mod tests { - use super::parse_nr_at; + use super::{Version, parse_nr_at}; use crate::number::MAX_SAFE_INTEGER; #[test] @@ -286,4 +412,14 @@ mod tests { assert_eq!(parse_nr_at(bytes, &mut pos).unwrap(), MAX_SAFE_INTEGER); assert_eq!(pos, bytes.len()); } + + #[test] + fn parse_covers_trimmed_fast_and_unicode_fallback_paths() { + assert_eq!(Version::parse(" 1.2.3 ").unwrap(), Version::new(1, 2, 3)); + assert_eq!(Version::parse(" 10.2.3 ").unwrap(), Version::new(10, 2, 3)); + assert_eq!( + Version::parse("\u{2003}1.2.3\u{2003}").unwrap(), + Version::new(1, 2, 3) + ); + } } diff --git a/tests/range.rs b/tests/range.rs index a27e049..db0c225 100644 --- a/tests/range.rs +++ b/tests/range.rs @@ -277,6 +277,7 @@ fn parse_valid_and_display_cases() { assert_display_case("^1.0.0", ">=1.0.0 <2.0.0-0"); assert_display_case("1.0.0", "1.0.0"); assert_display_case("=1.0.0", "1.0.0"); + assert_display_case("0", "<1.0.0-0"); assert_display_case("~0.x.0", "<1.0.0-0"); assert_display_case("~1.x.0", ">=1.0.0 <2.0.0-0"); assert_display_case("*", "*"); @@ -475,6 +476,9 @@ fn range_too_long() { assert!(Range::parse(&"^1.0.0 ".repeat(50)).is_err()); assert!(Range::parse(&format!(">= {}", "1".repeat(300))).is_err()); assert!(Range::parse(&format!("* || {}1", "v".repeat(300))).is_err()); + let mut long_bounded_range = "1.0.0 || ".repeat(29); + long_bounded_range.push_str("1.0.0"); + assert!(Range::parse(&long_bounded_range).is_err()); assert_eq!(Range::parse(LONG_VX_WILDCARD).unwrap().to_string(), "*"); } @@ -508,6 +512,9 @@ fn parse_invalid_cases() { assert_invalid_range("^"); assert_invalid_range("~"); assert_invalid_range("~="); + assert_invalid_range(">= || 1.0.0"); + assert_invalid_range("1.0.0 >="); + assert_invalid_range("10000000000000000"); assert_invalid_range("1.0.0 -"); assert_invalid_range("- 2.0.0"); assert_invalid_range("1.0.0 - 2.0.0 - 3.0.0"); diff --git a/tests/version.rs b/tests/version.rs index d07dabf..c148bd2 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -14,7 +14,7 @@ use core::cmp::Ordering; use core::fmt::{self, Write as _}; -use js_semver::Version; +use js_semver::{BuildMetadata, PreRelease, Version}; fn v(s: &str) -> Version { s.parse().unwrap() @@ -82,6 +82,21 @@ fn parse_valid_and_display_cases() { for (input, expected) in cases { assert_eq!(input.parse::().unwrap().to_string(), expected); } + + assert_eq!(Version::parse(" 1.2.3 ").unwrap(), Version::new(1, 2, 3)); + assert_eq!(Version::parse(" 10.2.3 ").unwrap(), Version::new(10, 2, 3)); + assert_eq!( + Version::parse("\u{2003}1.2.3\u{2003}").unwrap(), + Version::new(1, 2, 3) + ); +} + +#[test] +fn identifier_prefix_ordering() { + assert!(PreRelease::new("alpha").unwrap() < PreRelease::new("alpha.1").unwrap()); + assert!(PreRelease::new("alpha.1").unwrap() > PreRelease::new("alpha").unwrap()); + assert!(BuildMetadata::new("build").unwrap() < BuildMetadata::new("build.1").unwrap()); + assert!(BuildMetadata::new("build.1").unwrap() > BuildMetadata::new("build").unwrap()); } #[test]