diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2022-11-15 20:52:27 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-15 20:52:27 -0500 |
commit | 6da6ed89852ed40a984097d2341e43f8c263cee5 (patch) | |
tree | 55936261c24246cacc13cb01a35fa518ef94bb68 /cli/npm/semver/mod.rs | |
parent | 300fd07fad3dee74730517ba91b2e7314632fc3f (diff) |
fix(npm): support dist tags specified in npm package dependencies (#16652)
Closes #16321
Diffstat (limited to 'cli/npm/semver/mod.rs')
-rw-r--r-- | cli/npm/semver/mod.rs | 148 |
1 files changed, 129 insertions, 19 deletions
diff --git a/cli/npm/semver/mod.rs b/cli/npm/semver/mod.rs index cd63b2a29..c8c8e64fd 100644 --- a/cli/npm/semver/mod.rs +++ b/cli/npm/semver/mod.rs @@ -25,6 +25,14 @@ mod specifier; // A lot of the below is a re-implementation of parts of https://github.com/npm/node-semver // which is Copyright (c) Isaac Z. Schlueter and Contributors (ISC License) +pub fn is_valid_npm_tag(value: &str) -> bool { + // a valid tag is anything that doesn't get url encoded + // https://github.com/npm/npm-package-arg/blob/103c0fda8ed8185733919c7c6c73937cfb2baf3a/lib/npa.js#L399-L401 + value + .chars() + .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '~')) +} + #[derive( Clone, Debug, PartialEq, Eq, Default, Hash, Serialize, Deserialize, )] @@ -164,20 +172,35 @@ fn parse_npm_version(input: &str) -> ParseResult<NpmVersion> { )) } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum NpmVersionReqInner { + RangeSet(VersionRangeSet), + Tag(String), +} + /// A version requirement found in an npm package's dependencies. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NpmVersionReq { raw_text: String, - range_set: VersionRangeSet, + inner: NpmVersionReqInner, } impl NpmVersionMatcher for NpmVersionReq { fn tag(&self) -> Option<&str> { - None + match &self.inner { + NpmVersionReqInner::RangeSet(_) => None, + NpmVersionReqInner::Tag(tag) => Some(tag.as_str()), + } } fn matches(&self, version: &NpmVersion) -> bool { - self.satisfies(version) + match &self.inner { + NpmVersionReqInner::RangeSet(range_set) => range_set.satisfies(version), + NpmVersionReqInner::Tag(_) => panic!( + "programming error: cannot use matches with a tag: {}", + self.raw_text + ), + } } fn version_text(&self) -> String { @@ -197,16 +220,12 @@ impl NpmVersionReq { with_failure_handling(parse_npm_version_req)(text) .with_context(|| format!("Invalid npm version requirement '{}'.", text)) } - - pub fn satisfies(&self, version: &NpmVersion) -> bool { - self.range_set.satisfies(version) - } } fn parse_npm_version_req(input: &str) -> ParseResult<NpmVersionReq> { - map(range_set, |set| NpmVersionReq { + map(inner, |inner| NpmVersionReq { raw_text: input.to_string(), - range_set: set, + inner, })(input) } @@ -229,14 +248,97 @@ fn parse_npm_version_req(input: &str) -> ParseResult<NpmVersionReq> { // part ::= nr | [-0-9A-Za-z]+ // range-set ::= range ( logical-or range ) * -fn range_set(input: &str) -> ParseResult<VersionRangeSet> { +fn inner(input: &str) -> ParseResult<NpmVersionReqInner> { if input.is_empty() { - return Ok((input, VersionRangeSet(vec![VersionRange::all()]))); + return Ok(( + input, + NpmVersionReqInner::RangeSet(VersionRangeSet(vec![VersionRange::all()])), + )); + } + + let (input, mut ranges) = + separated_list(range_or_invalid, logical_or)(input)?; + + if ranges.len() == 1 { + match ranges.remove(0) { + RangeOrInvalid::Invalid(invalid) => { + if is_valid_npm_tag(invalid.text) { + return Ok(( + input, + NpmVersionReqInner::Tag(invalid.text.to_string()), + )); + } else { + return Err(invalid.failure); + } + } + RangeOrInvalid::Range(range) => { + // add it back + ranges.push(RangeOrInvalid::Range(range)); + } + } + } + + let ranges = ranges + .into_iter() + .filter_map(|r| r.into_range()) + .collect::<Vec<_>>(); + Ok((input, NpmVersionReqInner::RangeSet(VersionRangeSet(ranges)))) +} + +enum RangeOrInvalid<'a> { + Range(VersionRange), + Invalid(InvalidRange<'a>), +} + +impl<'a> RangeOrInvalid<'a> { + pub fn into_range(self) -> Option<VersionRange> { + match self { + RangeOrInvalid::Range(r) => { + if r.is_none() { + None + } else { + Some(r) + } + } + RangeOrInvalid::Invalid(_) => None, + } } - map(if_not_empty(separated_list(range, logical_or)), |ranges| { - // filter out the ranges that won't match anything for the tests - VersionRangeSet(ranges.into_iter().filter(|r| !r.is_none()).collect()) - })(input) +} + +struct InvalidRange<'a> { + failure: ParseError<'a>, + text: &'a str, +} + +fn range_or_invalid(input: &str) -> ParseResult<RangeOrInvalid> { + let range_result = + map_res(map(range, RangeOrInvalid::Range), |result| match result { + Ok((input, range)) => { + let is_end = input.is_empty() || logical_or(input).is_ok(); + if is_end { + Ok((input, range)) + } else { + ParseError::backtrace() + } + } + Err(err) => Err(err), + })(input); + match range_result { + Ok(result) => Ok(result), + Err(failure) => { + let (input, text) = invalid_range(input)?; + Ok(( + input, + RangeOrInvalid::Invalid(InvalidRange { failure, text }), + )) + } + } +} + +fn invalid_range(input: &str) -> ParseResult<&str> { + let end_index = input.find("||").unwrap_or(input.len()); + let text = input[..end_index].trim(); + Ok((&input[end_index..], text)) } // range ::= hyphen | simple ( ' ' simple ) * | '' @@ -505,7 +607,9 @@ mod tests { #[test] pub fn npm_version_req_ranges() { - let tester = NpmVersionReqTester::new(">= 2.1.2 < 3.0.0 || 5.x"); + let tester = NpmVersionReqTester::new( + ">= 2.1.2 < 3.0.0 || 5.x || ignored-invalid-range || $#$%^#$^#$^%@#$%SDF|||", + ); assert!(!tester.matches("2.1.1")); assert!(tester.matches("2.1.2")); assert!(tester.matches("2.9.9")); @@ -515,6 +619,12 @@ mod tests { assert!(!tester.matches("6.1.0")); } + #[test] + pub fn npm_version_req_with_tag() { + let req = NpmVersionReq::parse("latest").unwrap(); + assert_eq!(req.inner, NpmVersionReqInner::Tag("latest".to_string())); + } + macro_rules! assert_cmp { ($a:expr, $b:expr, $expected:expr) => { assert_eq!( @@ -729,7 +839,7 @@ mod tests { let range = NpmVersionReq::parse(range_text).unwrap(); let expected_range = NpmVersionReq::parse(expected).unwrap(); assert_eq!( - range.range_set, expected_range.range_set, + range.inner, expected_range.inner, "failed for {} and {}", range_text, expected ); @@ -853,7 +963,7 @@ mod tests { let req = NpmVersionReq::parse(req_text).unwrap(); let version = NpmVersion::parse(version_text).unwrap(); assert!( - req.satisfies(&version), + req.matches(&version), "Checking {} satisfies {}", req_text, version_text @@ -952,7 +1062,7 @@ mod tests { let req = NpmVersionReq::parse(req_text).unwrap(); let version = NpmVersion::parse(version_text).unwrap(); assert!( - !req.satisfies(&version), + !req.matches(&version), "Checking {} not satisfies {}", req_text, version_text |