diff --git a/.gitignore b/.gitignore index a9ed24baa..6ef97989d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /npm deno.lock +.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e40716fdd..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "deno.enable": true, - "deno.lint": true, - "deno.unstable": true -} diff --git a/Cargo.lock b/Cargo.lock index dd4ebe4ff..ba13715e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,12 +162,14 @@ dependencies = [ "data-url", "deno_ast", "futures", + "monch", "once_cell", "parking_lot", "pretty_assertions", "regex", "serde", "serde_json", + "thiserror", "tokio", "url", ] @@ -534,6 +536,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "monch" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13de1c3edc9a5b9dc3a1029f56e9ab3eba34640010aff4fc01044c42ef67afa" + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -1081,6 +1089,26 @@ dependencies = [ "serde", ] +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 87978621c..0343083ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,11 +20,13 @@ anyhow = "1.0.43" data-url = "0.2.0" deno_ast = { version = "0.24.0", features = ["dep_graph", "module_specifier"] } futures = "0.3.17" +monch = "0.4.0" once_cell = "1.16.0" parking_lot = "0.12.0" regex = "1.5.4" serde = { version = "1.0.130", features = ["derive", "rc"] } serde_json = { version = "1.0.67", features = ["preserve_order"] } +thiserror = "1.0.24" url = { version = "2.2.2", features = ["serde"] } [dev-dependencies] diff --git a/lib/deno_graph_wasm.generated.js b/lib/deno_graph_wasm.generated.js index 327002c68..cb2ccfb31 100644 --- a/lib/deno_graph_wasm.generated.js +++ b/lib/deno_graph_wasm.generated.js @@ -8,8 +8,24 @@ const heap = new Array(32).fill(undefined); heap.push(undefined, null, true, false); +function getObject(idx) { + return heap[idx]; +} + let heap_next = heap.length; +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); const idx = heap_next; @@ -19,8 +35,24 @@ function addHeapObject(obj) { return idx; } -function getObject(idx) { - return heap[idx]; +const cachedTextDecoder = new TextDecoder("utf-8", { + ignoreBOM: true, + fatal: true, +}); + +cachedTextDecoder.decode(); + +let cachedUint8Memory0 = new Uint8Array(); + +function getUint8Memory0() { + if (cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } function isLikeNone(x) { @@ -47,15 +79,6 @@ function getInt32Memory0() { let WASM_VECTOR_LEN = 0; -let cachedUint8Memory0 = new Uint8Array(); - -function getUint8Memory0() { - if (cachedUint8Memory0.byteLength === 0) { - cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8Memory0; -} - const cachedTextEncoder = new TextEncoder("utf-8"); const encodeString = function (arg, view) { @@ -99,29 +122,6 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } -const cachedTextDecoder = new TextDecoder("utf-8", { - ignoreBOM: true, - fatal: true, -}); - -cachedTextDecoder.decode(); - -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - -function dropObject(idx) { - if (idx < 36) return; - heap[idx] = heap_next; - heap_next = idx; -} - -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} - let cachedBigInt64Memory0 = new BigInt64Array(); function getBigInt64Memory0() { @@ -226,7 +226,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { } function __wbg_adapter_48(arg0, arg1, arg2) { wasm - ._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hec49a88a8b459e09( + ._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf0940dd4fdf51f25( arg0, arg1, addHeapObject(arg2), @@ -383,7 +383,7 @@ function handleError(f, args) { } } function __wbg_adapter_95(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h5fc0ee5e5d0575ff( + wasm.wasm_bindgen__convert__closures__invoke2_mut__h9adfd02b55150fae( arg0, arg1, addHeapObject(arg2), @@ -393,15 +393,50 @@ function __wbg_adapter_95(arg0, arg1, arg2, arg3) { const imports = { __wbindgen_placeholder__: { - __wbg_set_933729cf5b66ac11: function (arg0, arg1, arg2) { - const ret = getObject(arg0).set(getObject(arg1), getObject(arg2)); + __wbindgen_cb_drop: function (arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }, + __wbg_then_11f7a54d67b4bfad: function (arg0, arg1) { + const ret = getObject(arg0).then(getObject(arg1)); return addHeapObject(ret); }, - __wbg_set_20cbc34131e76824: function (arg0, arg1, arg2) { - getObject(arg0)[takeObject(arg1)] = takeObject(arg2); + __wbg_resolve_99fe17964f31ffc0: function (arg0) { + const ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); }, - __wbindgen_number_new: function (arg0) { - const ret = arg0; + __wbg_new_9962f939219f1820: function (arg0, arg1) { + try { + var state0 = { a: arg0, b: arg1 }; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_95(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } + }, + __wbindgen_object_drop_ref: function (arg0) { + takeObject(arg0); + }, + __wbg_length_6e3bbe7c8bd4dbd8: function (arg0) { + const ret = getObject(arg0).length; + return ret; + }, + __wbg_new_8d2af00bc1e329ee: function (arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }, __wbg_new_0b9bfdd97583284e: function () { @@ -415,16 +450,63 @@ const imports = { __wbg_set_a68214f35c417fa9: function (arg0, arg1, arg2) { getObject(arg0)[arg1 >>> 0] = takeObject(arg2); }, + __wbg_set_20cbc34131e76824: function (arg0, arg1, arg2) { + getObject(arg0)[takeObject(arg1)] = takeObject(arg2); + }, + __wbg_set_933729cf5b66ac11: function (arg0, arg1, arg2) { + const ret = getObject(arg0).set(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, + __wbg_String_91fba7ded13ba54c: function (arg0, arg1) { + const ret = String(getObject(arg1)); + const ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, + __wbindgen_number_new: function (arg0) { + const ret = arg0; + return addHeapObject(ret); + }, + __wbindgen_error_new: function (arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }, __wbindgen_is_string: function (arg0) { const ret = typeof (getObject(arg0)) === "string"; return ret; }, - __wbindgen_is_undefined: function (arg0) { - const ret = getObject(arg0) === undefined; + __wbindgen_bigint_from_u64: function (arg0) { + const ret = BigInt.asUintN(64, arg0); + return addHeapObject(ret); + }, + __wbg_getwithrefkey_15c62c2b8546208d: function (arg0, arg1) { + const ret = getObject(arg0)[getObject(arg1)]; + return addHeapObject(ret); + }, + __wbindgen_string_new: function (arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbg_then_cedad20fbbd9418a: function (arg0, arg1, arg2) { + const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, + __wbg_iterator_6f9d4f28845f426c: function () { + const ret = Symbol.iterator; + return addHeapObject(ret); + }, + __wbindgen_boolean_get: function (arg0) { + const v = getObject(arg0); + const ret = typeof (v) === "boolean" ? (v ? 1 : 0) : 2; return ret; }, - __wbindgen_in: function (arg0, arg1) { - const ret = getObject(arg0) in getObject(arg1); + __wbindgen_is_bigint: function (arg0) { + const ret = typeof (getObject(arg0)) === "bigint"; return ret; }, __wbindgen_number_get: function (arg0, arg1) { @@ -433,10 +515,9 @@ const imports = { getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); }, - __wbindgen_boolean_get: function (arg0) { - const v = getObject(arg0); - const ret = typeof (v) === "boolean" ? (v ? 1 : 0) : 2; - return ret; + __wbindgen_bigint_from_i64: function (arg0) { + const ret = arg0; + return addHeapObject(ret); }, __wbindgen_string_get: function (arg0, arg1) { const obj = getObject(arg1); @@ -452,91 +533,23 @@ const imports = { getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; }, - __wbindgen_is_bigint: function (arg0) { - const ret = typeof (getObject(arg0)) === "bigint"; - return ret; - }, __wbindgen_is_object: function (arg0) { const val = getObject(arg0); const ret = typeof (val) === "object" && val !== null; return ret; }, - __wbindgen_bigint_from_i64: function (arg0) { - const ret = arg0; - return addHeapObject(ret); - }, - __wbindgen_bigint_from_u64: function (arg0) { - const ret = BigInt.asUintN(64, arg0); - return addHeapObject(ret); - }, - __wbindgen_error_new: function (arg0, arg1) { - const ret = new Error(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }, - __wbg_String_91fba7ded13ba54c: function (arg0, arg1) { - const ret = String(getObject(arg1)); - const ptr0 = passStringToWasm0( - ret, - wasm.__wbindgen_malloc, - wasm.__wbindgen_realloc, - ); - const len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; + __wbindgen_in: function (arg0, arg1) { + const ret = getObject(arg0) in getObject(arg1); + return ret; }, __wbindgen_jsval_eq: function (arg0, arg1) { const ret = getObject(arg0) === getObject(arg1); return ret; }, - __wbindgen_object_drop_ref: function (arg0) { - takeObject(arg0); - }, - __wbg_getwithrefkey_15c62c2b8546208d: function (arg0, arg1) { - const ret = getObject(arg0)[getObject(arg1)]; - return addHeapObject(ret); - }, - __wbg_iterator_6f9d4f28845f426c: function () { - const ret = Symbol.iterator; - return addHeapObject(ret); - }, - __wbg_length_6e3bbe7c8bd4dbd8: function (arg0) { - const ret = getObject(arg0).length; + __wbindgen_is_undefined: function (arg0) { + const ret = getObject(arg0) === undefined; return ret; }, - __wbindgen_string_new: function (arg0, arg1) { - const ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }, - __wbg_resolve_99fe17964f31ffc0: function (arg0) { - const ret = Promise.resolve(getObject(arg0)); - return addHeapObject(ret); - }, - __wbg_then_cedad20fbbd9418a: function (arg0, arg1, arg2) { - const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); - return addHeapObject(ret); - }, - __wbg_new_8d2af00bc1e329ee: function (arg0, arg1) { - const ret = new Error(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }, - __wbg_new_9962f939219f1820: function (arg0, arg1) { - try { - var state0 = { a: arg0, b: arg1 }; - var cb0 = (arg0, arg1) => { - const a = state0.a; - state0.a = 0; - try { - return __wbg_adapter_95(a, state0.b, arg0, arg1); - } finally { - state0.a = a; - } - }; - const ret = new Promise(cb0); - return addHeapObject(ret); - } finally { - state0.a = state0.b = 0; - } - }, __wbg_get_57245cc7d7c7619d: function (arg0, arg1) { const ret = getObject(arg0)[arg1 >>> 0]; return addHeapObject(ret); @@ -674,21 +687,8 @@ const imports = { __wbindgen_throw: function (arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }, - __wbindgen_cb_drop: function (arg0) { - const obj = takeObject(arg0).original; - if (obj.cnt-- == 1) { - obj.a = 0; - return true; - } - const ret = false; - return ret; - }, - __wbg_then_11f7a54d67b4bfad: function (arg0, arg1) { - const ret = getObject(arg0).then(getObject(arg1)); - return addHeapObject(ret); - }, - __wbindgen_closure_wrapper440: function (arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 148, __wbg_adapter_48); + __wbindgen_closure_wrapper60: function (arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 3, __wbg_adapter_48); return addHeapObject(ret); }, }, diff --git a/lib/deno_graph_wasm_bg.wasm b/lib/deno_graph_wasm_bg.wasm index 964120d06..4ef0d85dd 100644 Binary files a/lib/deno_graph_wasm_bg.wasm and b/lib/deno_graph_wasm_bg.wasm differ diff --git a/src/lib.rs b/src/lib.rs index 8c6696bd8..815b61dc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod analyzer; mod ast; mod graph; mod module_specifier; +pub mod semver; pub mod source; mod text_encoding; diff --git a/src/semver/mod.rs b/src/semver/mod.rs new file mode 100644 index 000000000..0f96821db --- /dev/null +++ b/src/semver/mod.rs @@ -0,0 +1,204 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::cmp::Ordering; +use std::fmt; + +use serde::Deserialize; +use serde::Serialize; + +mod npm; +mod range; +mod specifier; + +use self::npm::parse_npm_version_req; +pub use self::npm::NpmVersionParseError; +pub use self::npm::NpmVersionReqParseError; +pub use self::range::Partial; +pub use self::range::VersionBoundKind; +pub use self::range::VersionRange; +pub use self::range::VersionRangeSet; +pub use self::range::XRange; +use self::specifier::parse_version_req_from_specifier; +pub use self::specifier::NpmVersionReqSpecifierParseError; + +#[derive( + Clone, Debug, PartialEq, Eq, Default, Hash, Serialize, Deserialize, +)] +pub struct Version { + pub major: u64, + pub minor: u64, + pub patch: u64, + pub pre: Vec, + pub build: Vec, +} + +impl Version { + pub fn parse_from_npm(text: &str) -> Result { + npm::parse_npm_version(text) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?; + if !self.pre.is_empty() { + write!(f, "-")?; + for (i, part) in self.pre.iter().enumerate() { + if i > 0 { + write!(f, ".")?; + } + write!(f, "{part}")?; + } + } + if !self.build.is_empty() { + write!(f, "+")?; + for (i, part) in self.build.iter().enumerate() { + if i > 0 { + write!(f, ".")?; + } + write!(f, "{part}")?; + } + } + Ok(()) + } +} + +impl std::cmp::PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::cmp::Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + let cmp_result = self.major.cmp(&other.major); + if cmp_result != Ordering::Equal { + return cmp_result; + } + + let cmp_result = self.minor.cmp(&other.minor); + if cmp_result != Ordering::Equal { + return cmp_result; + } + + let cmp_result = self.patch.cmp(&other.patch); + if cmp_result != Ordering::Equal { + return cmp_result; + } + + // only compare the pre-release and not the build as node-semver does + if self.pre.is_empty() && other.pre.is_empty() { + Ordering::Equal + } else if !self.pre.is_empty() && other.pre.is_empty() { + Ordering::Less + } else if self.pre.is_empty() && !other.pre.is_empty() { + Ordering::Greater + } else { + let mut i = 0; + loop { + let a = self.pre.get(i); + let b = other.pre.get(i); + if a.is_none() && b.is_none() { + return Ordering::Equal; + } + + // https://github.com/npm/node-semver/blob/4907647d169948a53156502867ed679268063a9f/internal/identifiers.js + let a = match a { + Some(a) => a, + None => return Ordering::Less, + }; + let b = match b { + Some(b) => b, + None => return Ordering::Greater, + }; + + // prefer numbers + if let Ok(a_num) = a.parse::() { + if let Ok(b_num) = b.parse::() { + return a_num.cmp(&b_num); + } else { + return Ordering::Less; + } + } else if b.parse::().is_ok() { + return Ordering::Greater; + } + + let cmp_result = a.cmp(b); + if cmp_result != Ordering::Equal { + return cmp_result; + } + i += 1; + } + } + } +} + +pub(super) fn is_valid_tag(value: &str) -> bool { + // we use the same rules as npm tags + npm::is_valid_npm_tag(value) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RangeSetOrTag { + RangeSet(VersionRangeSet), + Tag(String), +} + +/// A version requirement found in an npm package's dependencies. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VersionReq { + raw_text: String, + inner: RangeSetOrTag, +} + +impl VersionReq { + /// Creates a version requirement without examining the raw text. + pub fn from_raw_text_and_inner( + raw_text: String, + inner: RangeSetOrTag, + ) -> Self { + Self { raw_text, inner } + } + + pub fn parse_from_specifier( + specifier: &str, + ) -> Result { + parse_version_req_from_specifier(specifier) + } + + pub fn parse_from_npm(text: &str) -> Result { + parse_npm_version_req(text) + } + + #[cfg(test)] + pub(crate) fn inner(&self) -> &RangeSetOrTag { + &self.inner + } + + pub fn tag(&self) -> Option<&str> { + match &self.inner { + RangeSetOrTag::RangeSet(_) => None, + RangeSetOrTag::Tag(tag) => Some(tag.as_str()), + } + } + + pub fn matches(&self, version: &Version) -> bool { + match &self.inner { + RangeSetOrTag::RangeSet(range_set) => range_set.satisfies(version), + RangeSetOrTag::Tag(_) => panic!( + "programming error: cannot use matches with a tag: {}", + self.raw_text + ), + } + } + + pub fn version_text(&self) -> &str { + &self.raw_text + } +} + +impl fmt::Display for VersionReq { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.raw_text) + } +} diff --git a/src/semver/npm.rs b/src/semver/npm.rs new file mode 100644 index 000000000..c61b2a96f --- /dev/null +++ b/src/semver/npm.rs @@ -0,0 +1,1000 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use monch::*; +use thiserror::Error; + +use super::Partial; +use super::RangeSetOrTag; +use super::Version; +use super::VersionBoundKind; +use super::VersionRange; +use super::VersionRangeSet; +use super::VersionReq; +use super::XRange; + +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, '-' | '_' | '.' | '~')) +} + +// 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) + +#[derive(Error, Debug)] +#[error("Invalid npm version '{source}'.")] +pub struct NpmVersionParseError { + #[source] + source: ParseErrorFailureError, +} + +pub fn parse_npm_version(text: &str) -> Result { + let text = text.trim(); + with_failure_handling(|input| { + let (input, _) = maybe(ch('='))(input)?; // skip leading = + let (input, _) = skip_whitespace(input)?; + let (input, _) = maybe(ch('v'))(input)?; // skip leading v + let (input, _) = skip_whitespace(input)?; + let (input, major) = nr(input)?; + let (input, _) = ch('.')(input)?; + let (input, minor) = nr(input)?; + let (input, _) = ch('.')(input)?; + let (input, patch) = nr(input)?; + let (input, q) = maybe(qualifier)(input)?; + let q = q.unwrap_or_default(); + + Ok(( + input, + Version { + major, + minor, + patch, + pre: q.pre, + build: q.build, + }, + )) + })(text) + .map_err(|err| NpmVersionParseError { source: err }) +} + +#[derive(Error, Debug)] +#[error("Invalid npm version requirement '{source}'.")] +pub struct NpmVersionReqParseError { + #[source] + source: ParseErrorFailureError, +} + +pub fn parse_npm_version_req( + text: &str, +) -> Result { + let text = text.trim(); + with_failure_handling(|input| { + map(inner, |inner| { + VersionReq::from_raw_text_and_inner(input.to_string(), inner) + })(input) + })(text) + .map_err(|err| NpmVersionReqParseError { source: err }) +} + +// https://github.com/npm/node-semver/tree/4907647d169948a53156502867ed679268063a9f#range-grammar +// range-set ::= range ( logical-or range ) * +// logical-or ::= ( ' ' ) * '||' ( ' ' ) * +// range ::= hyphen | simple ( ' ' simple ) * | '' +// hyphen ::= partial ' - ' partial +// simple ::= primitive | partial | tilde | caret +// primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial +// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? +// xr ::= 'x' | 'X' | '*' | nr +// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * +// tilde ::= '~' partial +// caret ::= '^' partial +// qualifier ::= ( '-' pre )? ( '+' build )? +// pre ::= parts +// build ::= parts +// parts ::= part ( '.' part ) * +// part ::= nr | [-0-9A-Za-z]+ + +// range-set ::= range ( logical-or range ) * +fn inner(input: &str) -> ParseResult { + if input.is_empty() { + return Ok(( + input, + RangeSetOrTag::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, RangeSetOrTag::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::>(); + Ok((input, RangeSetOrTag::RangeSet(VersionRangeSet(ranges)))) +} + +enum RangeOrInvalid<'a> { + Range(VersionRange), + Invalid(InvalidRange<'a>), +} + +impl<'a> RangeOrInvalid<'a> { + pub fn into_range(self) -> Option { + match self { + RangeOrInvalid::Range(r) => { + if r.is_none() { + None + } else { + Some(r) + } + } + RangeOrInvalid::Invalid(_) => None, + } + } +} + +struct InvalidRange<'a> { + failure: ParseError<'a>, + text: &'a str, +} + +fn range_or_invalid(input: &str) -> ParseResult { + 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 ) * | '' +fn range(input: &str) -> ParseResult { + or( + map(hyphen, |hyphen| VersionRange { + start: hyphen.start.as_lower_bound(), + end: hyphen.end.as_upper_bound(), + }), + map(separated_list(simple, whitespace), |ranges| { + let mut final_range = VersionRange::all(); + for range in ranges { + final_range = final_range.clamp(&range); + } + final_range + }), + )(input) +} + +#[derive(Debug, Clone)] +struct Hyphen { + start: Partial, + end: Partial, +} + +// hyphen ::= partial ' - ' partial +fn hyphen(input: &str) -> ParseResult { + let (input, first) = partial(input)?; + let (input, _) = whitespace(input)?; + let (input, _) = tag("-")(input)?; + let (input, _) = whitespace(input)?; + let (input, second) = partial(input)?; + Ok(( + input, + Hyphen { + start: first, + end: second, + }, + )) +} + +// logical-or ::= ( ' ' ) * '||' ( ' ' ) * +fn logical_or(input: &str) -> ParseResult<&str> { + delimited(skip_whitespace, tag("||"), skip_whitespace)(input) +} + +fn skip_whitespace_or_v(input: &str) -> ParseResult<()> { + map( + pair(skip_whitespace, pair(maybe(ch('v')), skip_whitespace)), + |_| (), + )(input) +} + +// simple ::= primitive | partial | tilde | caret +fn simple(input: &str) -> ParseResult { + or4( + map(preceded(tilde, partial), |partial| { + partial.as_tilde_version_range() + }), + map(preceded(caret, partial), |partial| { + partial.as_caret_version_range() + }), + map(primitive, |primitive| { + let partial = primitive.partial; + match primitive.kind { + PrimitiveKind::Equal => partial.as_equal_range(), + PrimitiveKind::GreaterThan => { + partial.as_greater_than(VersionBoundKind::Exclusive) + } + PrimitiveKind::GreaterThanOrEqual => { + partial.as_greater_than(VersionBoundKind::Inclusive) + } + PrimitiveKind::LessThan => { + partial.as_less_than(VersionBoundKind::Exclusive) + } + PrimitiveKind::LessThanOrEqual => { + partial.as_less_than(VersionBoundKind::Inclusive) + } + } + }), + map(partial, |partial| partial.as_equal_range()), + )(input) +} + +fn tilde(input: &str) -> ParseResult<()> { + fn raw_tilde(input: &str) -> ParseResult<()> { + map(pair(or(tag("~>"), tag("~")), skip_whitespace_or_v), |_| ())(input) + } + + or( + preceded(terminated(primitive_kind, whitespace), raw_tilde), + raw_tilde, + )(input) +} + +fn caret(input: &str) -> ParseResult<()> { + fn raw_caret(input: &str) -> ParseResult<()> { + map(pair(ch('^'), skip_whitespace_or_v), |_| ())(input) + } + + or( + preceded(terminated(primitive_kind, whitespace), raw_caret), + raw_caret, + )(input) +} + +#[derive(Debug, Clone, Copy)] +enum PrimitiveKind { + GreaterThan, + LessThan, + GreaterThanOrEqual, + LessThanOrEqual, + Equal, +} + +#[derive(Debug, Clone)] +struct Primitive { + kind: PrimitiveKind, + partial: Partial, +} + +fn primitive(input: &str) -> ParseResult { + let (input, kind) = primitive_kind(input)?; + let (input, _) = skip_whitespace(input)?; + let (input, partial) = partial(input)?; + Ok((input, Primitive { kind, partial })) +} + +fn primitive_kind(input: &str) -> ParseResult { + or5( + map(tag(">="), |_| PrimitiveKind::GreaterThanOrEqual), + map(tag("<="), |_| PrimitiveKind::LessThanOrEqual), + map(ch('<'), |_| PrimitiveKind::LessThan), + map(ch('>'), |_| PrimitiveKind::GreaterThan), + map(ch('='), |_| PrimitiveKind::Equal), + )(input) +} + +// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? +fn partial(input: &str) -> ParseResult { + let (input, _) = maybe(ch('v'))(input)?; // skip leading v + let (input, major) = xr()(input)?; + let (input, maybe_minor) = maybe(preceded(ch('.'), xr()))(input)?; + let (input, maybe_patch) = if maybe_minor.is_some() { + maybe(preceded(ch('.'), xr()))(input)? + } else { + (input, None) + }; + let (input, qual) = if maybe_patch.is_some() { + maybe(qualifier)(input)? + } else { + (input, None) + }; + let qual = qual.unwrap_or_default(); + Ok(( + input, + Partial { + major, + minor: maybe_minor.unwrap_or(XRange::Wildcard), + patch: maybe_patch.unwrap_or(XRange::Wildcard), + pre: qual.pre, + build: qual.build, + }, + )) +} + +// xr ::= 'x' | 'X' | '*' | nr +fn xr<'a>() -> impl Fn(&'a str) -> ParseResult<'a, XRange> { + or( + map(or3(tag("x"), tag("X"), tag("*")), |_| XRange::Wildcard), + map(nr, XRange::Val), + ) +} + +// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * +fn nr(input: &str) -> ParseResult { + // we do loose parsing to support people doing stuff like 01.02.03 + let (input, result) = + if_not_empty(substring(skip_while(|c| c.is_ascii_digit())))(input)?; + let val = match result.parse::() { + Ok(val) => val, + Err(err) => { + return ParseError::fail( + input, + format!("Error parsing '{result}' to u64.\n\n{err:#}"), + ) + } + }; + Ok((input, val)) +} + +#[derive(Debug, Clone, Default)] +struct Qualifier { + pre: Vec, + build: Vec, +} + +// qualifier ::= ( '-' pre )? ( '+' build )? +fn qualifier(input: &str) -> ParseResult { + let (input, pre_parts) = maybe(pre)(input)?; + let (input, build_parts) = maybe(build)(input)?; + Ok(( + input, + Qualifier { + pre: pre_parts.unwrap_or_default(), + build: build_parts.unwrap_or_default(), + }, + )) +} + +// pre ::= parts +fn pre(input: &str) -> ParseResult> { + preceded(maybe(ch('-')), parts)(input) +} + +// build ::= parts +fn build(input: &str) -> ParseResult> { + preceded(ch('+'), parts)(input) +} + +// parts ::= part ( '.' part ) * +fn parts(input: &str) -> ParseResult> { + if_not_empty(map(separated_list(part, ch('.')), |text| { + text.into_iter().map(ToOwned::to_owned).collect() + }))(input) +} + +// part ::= nr | [-0-9A-Za-z]+ +fn part(input: &str) -> ParseResult<&str> { + // nr is in the other set, so don't bother checking for it + if_true( + take_while(|c| c.is_ascii_alphanumeric() || c == '-'), + |result| !result.is_empty(), + )(input) +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use std::cmp::Ordering; + + use super::*; + + struct NpmVersionReqTester(VersionReq); + + impl NpmVersionReqTester { + fn new(text: &str) -> Self { + Self(parse_npm_version_req(text).unwrap()) + } + + fn matches(&self, version: &str) -> bool { + self.0.matches(&parse_npm_version(version).unwrap()) + } + } + + #[test] + pub fn npm_version_req_with_v() { + assert!(parse_npm_version_req("v1.0.0").is_ok()); + } + + #[test] + pub fn npm_version_req_exact() { + let tester = NpmVersionReqTester::new("2.1.2"); + assert!(!tester.matches("2.1.1")); + assert!(tester.matches("2.1.2")); + assert!(!tester.matches("2.1.3")); + + let tester = NpmVersionReqTester::new("2.1.2 || 2.1.5"); + assert!(!tester.matches("2.1.1")); + assert!(tester.matches("2.1.2")); + assert!(!tester.matches("2.1.3")); + assert!(!tester.matches("2.1.4")); + assert!(tester.matches("2.1.5")); + assert!(!tester.matches("2.1.6")); + } + + #[test] + pub fn npm_version_req_minor() { + let tester = NpmVersionReqTester::new("1.1"); + assert!(!tester.matches("1.0.0")); + assert!(tester.matches("1.1.0")); + assert!(tester.matches("1.1.1")); + assert!(!tester.matches("1.2.0")); + assert!(!tester.matches("1.2.1")); + } + + #[test] + pub fn npm_version_req_ranges() { + 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")); + assert!(!tester.matches("3.0.0")); + assert!(tester.matches("5.0.0")); + assert!(tester.matches("5.1.0")); + assert!(!tester.matches("6.1.0")); + } + + #[test] + pub fn npm_version_req_with_tag() { + let req = parse_npm_version_req("latest").unwrap(); + assert_eq!(req.tag(), Some("latest")); + } + + macro_rules! assert_cmp { + ($a:expr, $b:expr, $expected:expr) => { + assert_eq!( + $a.cmp(&$b), + $expected, + "expected {} to be {:?} {}", + $a, + $expected, + $b + ); + }; + } + + macro_rules! test_compare { + ($a:expr, $b:expr, $expected:expr) => { + let a = parse_npm_version($a).unwrap(); + let b = parse_npm_version($b).unwrap(); + assert_cmp!(a, b, $expected); + }; + } + + #[test] + fn version_compare() { + test_compare!("1.2.3", "2.3.4", Ordering::Less); + test_compare!("1.2.3", "1.2.4", Ordering::Less); + test_compare!("1.2.3", "1.2.3", Ordering::Equal); + test_compare!("1.2.3", "1.2.2", Ordering::Greater); + test_compare!("1.2.3", "1.1.5", Ordering::Greater); + } + + #[test] + fn version_compare_equal() { + // https://github.com/npm/node-semver/blob/bce42589d33e1a99454530a8fd52c7178e2b11c1/test/fixtures/equality.js + let fixtures = &[ + ("1.2.3", "v1.2.3"), + ("1.2.3", "=1.2.3"), + ("1.2.3", "v 1.2.3"), + ("1.2.3", "= 1.2.3"), + ("1.2.3", " v1.2.3"), + ("1.2.3", " =1.2.3"), + ("1.2.3", " v 1.2.3"), + ("1.2.3", " = 1.2.3"), + ("1.2.3-0", "v1.2.3-0"), + ("1.2.3-0", "=1.2.3-0"), + ("1.2.3-0", "v 1.2.3-0"), + ("1.2.3-0", "= 1.2.3-0"), + ("1.2.3-0", " v1.2.3-0"), + ("1.2.3-0", " =1.2.3-0"), + ("1.2.3-0", " v 1.2.3-0"), + ("1.2.3-0", " = 1.2.3-0"), + ("1.2.3-1", "v1.2.3-1"), + ("1.2.3-1", "=1.2.3-1"), + ("1.2.3-1", "v 1.2.3-1"), + ("1.2.3-1", "= 1.2.3-1"), + ("1.2.3-1", " v1.2.3-1"), + ("1.2.3-1", " =1.2.3-1"), + ("1.2.3-1", " v 1.2.3-1"), + ("1.2.3-1", " = 1.2.3-1"), + ("1.2.3-beta", "v1.2.3-beta"), + ("1.2.3-beta", "=1.2.3-beta"), + ("1.2.3-beta", "v 1.2.3-beta"), + ("1.2.3-beta", "= 1.2.3-beta"), + ("1.2.3-beta", " v1.2.3-beta"), + ("1.2.3-beta", " =1.2.3-beta"), + ("1.2.3-beta", " v 1.2.3-beta"), + ("1.2.3-beta", " = 1.2.3-beta"), + ("1.2.3-beta+build", " = 1.2.3-beta+otherbuild"), + ("1.2.3+build", " = 1.2.3+otherbuild"), + ("1.2.3-beta+build", "1.2.3-beta+otherbuild"), + ("1.2.3+build", "1.2.3+otherbuild"), + (" v1.2.3+build", "1.2.3+otherbuild"), + ]; + for (a, b) in fixtures { + test_compare!(a, b, Ordering::Equal); + } + } + + #[test] + fn version_comparisons_test() { + // https://github.com/npm/node-semver/blob/bce42589d33e1a99454530a8fd52c7178e2b11c1/test/fixtures/comparisons.js + let fixtures = &[ + ("0.0.0", "0.0.0-foo"), + ("0.0.1", "0.0.0"), + ("1.0.0", "0.9.9"), + ("0.10.0", "0.9.0"), + ("0.99.0", "0.10.0"), + ("2.0.0", "1.2.3"), + ("v0.0.0", "0.0.0-foo"), + ("v0.0.1", "0.0.0"), + ("v1.0.0", "0.9.9"), + ("v0.10.0", "0.9.0"), + ("v0.99.0", "0.10.0"), + ("v2.0.0", "1.2.3"), + ("0.0.0", "v0.0.0-foo"), + ("0.0.1", "v0.0.0"), + ("1.0.0", "v0.9.9"), + ("0.10.0", "v0.9.0"), + ("0.99.0", "v0.10.0"), + ("2.0.0", "v1.2.3"), + ("1.2.3", "1.2.3-asdf"), + ("1.2.3", "1.2.3-4"), + ("1.2.3", "1.2.3-4-foo"), + ("1.2.3-5-foo", "1.2.3-5"), + ("1.2.3-5", "1.2.3-4"), + ("1.2.3-5-foo", "1.2.3-5-Foo"), + ("3.0.0", "2.7.2+asdf"), + ("1.2.3-a.10", "1.2.3-a.5"), + ("1.2.3-a.b", "1.2.3-a.5"), + ("1.2.3-a.b", "1.2.3-a"), + ("1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"), + ("1.2.3-r2", "1.2.3-r100"), + ("1.2.3-r100", "1.2.3-R2"), + ]; + for (a, b) in fixtures { + let a = parse_npm_version(a).unwrap(); + let b = parse_npm_version(b).unwrap(); + assert_cmp!(a, b, Ordering::Greater); + assert_cmp!(b, a, Ordering::Less); + assert_cmp!(a, a, Ordering::Equal); + assert_cmp!(b, b, Ordering::Equal); + } + } + + #[test] + fn range_parse() { + // https://github.com/npm/node-semver/blob/4907647d169948a53156502867ed679268063a9f/test/fixtures/range-parse.js + let fixtures = &[ + ("1.0.0 - 2.0.0", ">=1.0.0 <=2.0.0"), + ("1 - 2", ">=1.0.0 <3.0.0-0"), + ("1.0 - 2.0", ">=1.0.0 <2.1.0-0"), + ("1.0.0", "1.0.0"), + (">=*", "*"), + ("", "*"), + ("*", "*"), + ("*", "*"), + (">=1.0.0", ">=1.0.0"), + (">1.0.0", ">1.0.0"), + ("<=2.0.0", "<=2.0.0"), + ("1", ">=1.0.0 <2.0.0-0"), + ("<=2.0.0", "<=2.0.0"), + ("<=2.0.0", "<=2.0.0"), + ("<2.0.0", "<2.0.0"), + ("<2.0.0", "<2.0.0"), + (">= 1.0.0", ">=1.0.0"), + (">= 1.0.0", ">=1.0.0"), + (">= 1.0.0", ">=1.0.0"), + ("> 1.0.0", ">1.0.0"), + ("> 1.0.0", ">1.0.0"), + ("<= 2.0.0", "<=2.0.0"), + ("<= 2.0.0", "<=2.0.0"), + ("<= 2.0.0", "<=2.0.0"), + ("< 2.0.0", "<2.0.0"), + ("<\t2.0.0", "<2.0.0"), + (">=0.1.97", ">=0.1.97"), + (">=0.1.97", ">=0.1.97"), + ("0.1.20 || 1.2.4", "0.1.20||1.2.4"), + (">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"), + (">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"), + (">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"), + ("||", "*"), + ("2.x.x", ">=2.0.0 <3.0.0-0"), + ("1.2.x", ">=1.2.0 <1.3.0-0"), + ("1.2.x || 2.x", ">=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0"), + ("1.2.x || 2.x", ">=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0"), + ("x", "*"), + ("2.*.*", ">=2.0.0 <3.0.0-0"), + ("1.2.*", ">=1.2.0 <1.3.0-0"), + ("1.2.* || 2.*", ">=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0"), + ("*", "*"), + ("2", ">=2.0.0 <3.0.0-0"), + ("2.3", ">=2.3.0 <2.4.0-0"), + ("~2.4", ">=2.4.0 <2.5.0-0"), + ("~2.4", ">=2.4.0 <2.5.0-0"), + ("~>3.2.1", ">=3.2.1 <3.3.0-0"), + ("~1", ">=1.0.0 <2.0.0-0"), + ("~>1", ">=1.0.0 <2.0.0-0"), + ("~> 1", ">=1.0.0 <2.0.0-0"), + ("~1.0", ">=1.0.0 <1.1.0-0"), + ("~ 1.0", ">=1.0.0 <1.1.0-0"), + ("^0", "<1.0.0-0"), + ("^ 1", ">=1.0.0 <2.0.0-0"), + ("^0.1", ">=0.1.0 <0.2.0-0"), + ("^1.0", ">=1.0.0 <2.0.0-0"), + ("^1.2", ">=1.2.0 <2.0.0-0"), + ("^0.0.1", ">=0.0.1 <0.0.2-0"), + ("^0.0.1-beta", ">=0.0.1-beta <0.0.2-0"), + ("^0.1.2", ">=0.1.2 <0.2.0-0"), + ("^1.2.3", ">=1.2.3 <2.0.0-0"), + ("^1.2.3-beta.4", ">=1.2.3-beta.4 <2.0.0-0"), + ("<1", "<1.0.0-0"), + ("< 1", "<1.0.0-0"), + (">=1", ">=1.0.0"), + (">= 1", ">=1.0.0"), + ("<1.2", "<1.2.0-0"), + ("< 1.2", "<1.2.0-0"), + ("1", ">=1.0.0 <2.0.0-0"), + ("^ 1.2 ^ 1", ">=1.2.0 <2.0.0-0 >=1.0.0"), + ("1.2 - 3.4.5", ">=1.2.0 <=3.4.5"), + ("1.2.3 - 3.4", ">=1.2.3 <3.5.0-0"), + ("1.2 - 3.4", ">=1.2.0 <3.5.0-0"), + (">1", ">=2.0.0"), + (">1.2", ">=1.3.0"), + (">X", "<0.0.0-0"), + ("* 2.x", "<0.0.0-0"), + (">x 2.x || * || 01.02.03", ">1.2.3"), + ("~1.2.3beta", ">=1.2.3-beta <1.3.0-0"), + (">=09090", ">=9090.0.0"), + ]; + for (range_text, expected) in fixtures { + let range = parse_npm_version_req(range_text).unwrap(); + let expected_range = parse_npm_version_req(expected).unwrap(); + assert_eq!( + range.inner(), + expected_range.inner(), + "failed for {} and {}", + range_text, + expected + ); + } + } + + #[test] + fn range_satisfies() { + // https://github.com/npm/node-semver/blob/4907647d169948a53156502867ed679268063a9f/test/fixtures/range-include.js + let fixtures = &[ + ("1.0.0 - 2.0.0", "1.2.3"), + ("^1.2.3+build", "1.2.3"), + ("^1.2.3+build", "1.3.0"), + ("1.2.3-pre+asdf - 2.4.3-pre+asdf", "1.2.3"), + ("1.2.3pre+asdf - 2.4.3-pre+asdf", "1.2.3"), + ("1.2.3-pre+asdf - 2.4.3pre+asdf", "1.2.3"), + ("1.2.3pre+asdf - 2.4.3pre+asdf", "1.2.3"), + ("1.2.3-pre+asdf - 2.4.3-pre+asdf", "1.2.3-pre.2"), + ("1.2.3-pre+asdf - 2.4.3-pre+asdf", "2.4.3-alpha"), + ("1.2.3+asdf - 2.4.3+asdf", "1.2.3"), + ("1.0.0", "1.0.0"), + (">=*", "0.2.4"), + ("", "1.0.0"), + ("*", "1.2.3"), + ("*", "v1.2.3"), + (">=1.0.0", "1.0.0"), + (">=1.0.0", "1.0.1"), + (">=1.0.0", "1.1.0"), + (">1.0.0", "1.0.1"), + (">1.0.0", "1.1.0"), + ("<=2.0.0", "2.0.0"), + ("<=2.0.0", "1.9999.9999"), + ("<=2.0.0", "0.2.9"), + ("<2.0.0", "1.9999.9999"), + ("<2.0.0", "0.2.9"), + (">= 1.0.0", "1.0.0"), + (">= 1.0.0", "1.0.1"), + (">= 1.0.0", "1.1.0"), + ("> 1.0.0", "1.0.1"), + ("> 1.0.0", "1.1.0"), + ("<= 2.0.0", "2.0.0"), + ("<= 2.0.0", "1.9999.9999"), + ("<= 2.0.0", "0.2.9"), + ("< 2.0.0", "1.9999.9999"), + ("<\t2.0.0", "0.2.9"), + (">=0.1.97", "v0.1.97"), + (">=0.1.97", "0.1.97"), + ("0.1.20 || 1.2.4", "1.2.4"), + (">=0.2.3 || <0.0.1", "0.0.0"), + (">=0.2.3 || <0.0.1", "0.2.3"), + (">=0.2.3 || <0.0.1", "0.2.4"), + ("||", "1.3.4"), + ("2.x.x", "2.1.3"), + ("1.2.x", "1.2.3"), + ("1.2.x || 2.x", "2.1.3"), + ("1.2.x || 2.x", "1.2.3"), + ("x", "1.2.3"), + ("2.*.*", "2.1.3"), + ("1.2.*", "1.2.3"), + ("1.2.* || 2.*", "2.1.3"), + ("1.2.* || 2.*", "1.2.3"), + ("*", "1.2.3"), + ("2", "2.1.2"), + ("2.3", "2.3.1"), + ("~0.0.1", "0.0.1"), + ("~0.0.1", "0.0.2"), + ("~x", "0.0.9"), // >=2.4.0 <2.5.0 + ("~2", "2.0.9"), // >=2.4.0 <2.5.0 + ("~2.4", "2.4.0"), // >=2.4.0 <2.5.0 + ("~2.4", "2.4.5"), + ("~>3.2.1", "3.2.2"), // >=3.2.1 <3.3.0, + ("~1", "1.2.3"), // >=1.0.0 <2.0.0 + ("~>1", "1.2.3"), + ("~> 1", "1.2.3"), + ("~1.0", "1.0.2"), // >=1.0.0 <1.1.0, + ("~ 1.0", "1.0.2"), + ("~ 1.0.3", "1.0.12"), + ("~ 1.0.3alpha", "1.0.12"), + (">=1", "1.0.0"), + (">= 1", "1.0.0"), + ("<1.2", "1.1.1"), + ("< 1.2", "1.1.1"), + ("~v0.5.4-pre", "0.5.5"), + ("~v0.5.4-pre", "0.5.4"), + ("=0.7.x", "0.7.2"), + ("<=0.7.x", "0.7.2"), + (">=0.7.x", "0.7.2"), + ("<=0.7.x", "0.6.2"), + ("~1.2.1 >=1.2.3", "1.2.3"), + ("~1.2.1 =1.2.3", "1.2.3"), + ("~1.2.1 1.2.3", "1.2.3"), + ("~1.2.1 >=1.2.3 1.2.3", "1.2.3"), + ("~1.2.1 1.2.3 >=1.2.3", "1.2.3"), + ("~1.2.1 1.2.3", "1.2.3"), + (">=1.2.1 1.2.3", "1.2.3"), + ("1.2.3 >=1.2.1", "1.2.3"), + (">=1.2.3 >=1.2.1", "1.2.3"), + (">=1.2.1 >=1.2.3", "1.2.3"), + (">=1.2", "1.2.8"), + ("^1.2.3", "1.8.1"), + ("^0.1.2", "0.1.2"), + ("^0.1", "0.1.2"), + ("^0.0.1", "0.0.1"), + ("^1.2", "1.4.2"), + ("^1.2 ^1", "1.4.2"), + ("^1.2.3-alpha", "1.2.3-pre"), + ("^1.2.0-alpha", "1.2.0-pre"), + ("^0.0.1-alpha", "0.0.1-beta"), + ("^0.0.1-alpha", "0.0.1"), + ("^0.1.1-alpha", "0.1.1-beta"), + ("^x", "1.2.3"), + ("x - 1.0.0", "0.9.7"), + ("x - 1.x", "0.9.7"), + ("1.0.0 - x", "1.9.7"), + ("1.x - x", "1.9.7"), + ("<=7.x", "7.9.9"), + // additional tests + ("1.0.0-alpha.13", "1.0.0-alpha.13"), + ]; + for (req_text, version_text) in fixtures { + let req = parse_npm_version_req(req_text).unwrap(); + let version = parse_npm_version(version_text).unwrap(); + assert!( + req.matches(&version), + "Checking {req_text} satisfies {version_text}" + ); + } + } + + #[test] + fn range_not_satisfies() { + let fixtures = &[ + ("1.0.0 - 2.0.0", "2.2.3"), + ("1.2.3+asdf - 2.4.3+asdf", "1.2.3-pre.2"), + ("1.2.3+asdf - 2.4.3+asdf", "2.4.3-alpha"), + ("^1.2.3+build", "2.0.0"), + ("^1.2.3+build", "1.2.0"), + ("^1.2.3", "1.2.3-pre"), + ("^1.2", "1.2.0-pre"), + (">1.2", "1.3.0-beta"), + ("<=1.2.3", "1.2.3-beta"), + ("^1.2.3", "1.2.3-beta"), + ("=0.7.x", "0.7.0-asdf"), + (">=0.7.x", "0.7.0-asdf"), + ("<=0.7.x", "0.7.0-asdf"), + ("1", "1.0.0beta"), + ("<1", "1.0.0beta"), + ("< 1", "1.0.0beta"), + ("1.0.0", "1.0.1"), + (">=1.0.0", "0.0.0"), + (">=1.0.0", "0.0.1"), + (">=1.0.0", "0.1.0"), + (">1.0.0", "0.0.1"), + (">1.0.0", "0.1.0"), + ("<=2.0.0", "3.0.0"), + ("<=2.0.0", "2.9999.9999"), + ("<=2.0.0", "2.2.9"), + ("<2.0.0", "2.9999.9999"), + ("<2.0.0", "2.2.9"), + (">=0.1.97", "v0.1.93"), + (">=0.1.97", "0.1.93"), + ("0.1.20 || 1.2.4", "1.2.3"), + (">=0.2.3 || <0.0.1", "0.0.3"), + (">=0.2.3 || <0.0.1", "0.2.2"), + ("2.x.x", "1.1.3"), + ("2.x.x", "3.1.3"), + ("1.2.x", "1.3.3"), + ("1.2.x || 2.x", "3.1.3"), + ("1.2.x || 2.x", "1.1.3"), + ("2.*.*", "1.1.3"), + ("2.*.*", "3.1.3"), + ("1.2.*", "1.3.3"), + ("1.2.* || 2.*", "3.1.3"), + ("1.2.* || 2.*", "1.1.3"), + ("2", "1.1.2"), + ("2.3", "2.4.1"), + ("~0.0.1", "0.1.0-alpha"), + ("~0.0.1", "0.1.0"), + ("~2.4", "2.5.0"), // >=2.4.0 <2.5.0 + ("~2.4", "2.3.9"), + ("~>3.2.1", "3.3.2"), // >=3.2.1 <3.3.0 + ("~>3.2.1", "3.2.0"), // >=3.2.1 <3.3.0 + ("~1", "0.2.3"), // >=1.0.0 <2.0.0 + ("~>1", "2.2.3"), + ("~1.0", "1.1.0"), // >=1.0.0 <1.1.0 + ("<1", "1.0.0"), + (">=1.2", "1.1.1"), + ("1", "2.0.0beta"), + ("~v0.5.4-beta", "0.5.4-alpha"), + ("=0.7.x", "0.8.2"), + (">=0.7.x", "0.6.2"), + ("<0.7.x", "0.7.2"), + ("<1.2.3", "1.2.3-beta"), + ("=1.2.3", "1.2.3-beta"), + (">1.2", "1.2.8"), + ("^0.0.1", "0.0.2-alpha"), + ("^0.0.1", "0.0.2"), + ("^1.2.3", "2.0.0-alpha"), + ("^1.2.3", "1.2.2"), + ("^1.2", "1.1.9"), + ("*", "v1.2.3-foo"), + ("^1.0.0", "2.0.0-rc1"), + ("1 - 2", "2.0.0-pre"), + ("1 - 2", "1.0.0-pre"), + ("1.0 - 2", "1.0.0-pre"), + ("1.1.x", "1.0.0-a"), + ("1.1.x", "1.1.0-a"), + ("1.1.x", "1.2.0-a"), + ("1.x", "1.0.0-a"), + ("1.x", "1.1.0-a"), + ("1.x", "1.2.0-a"), + (">=1.0.0 <1.1.0", "1.1.0"), + (">=1.0.0 <1.1.0", "1.1.0-pre"), + (">=1.0.0 <1.1.0-pre", "1.1.0-pre"), + ]; + + for (req_text, version_text) in fixtures { + let req = parse_npm_version_req(req_text).unwrap(); + let version = parse_npm_version(version_text).unwrap(); + assert!( + !req.matches(&version), + "Checking {req_text} not satisfies {version_text}" + ); + } + } + + #[test] + fn range_primitive_kind_beside_caret_or_tilde_with_whitespace() { + // node semver should have enforced strictness, but it didn't + // and so we end up with a system that acts this way + let fixtures = &[ + (">= ^1.2.3", "1.2.3", true), + (">= ^1.2.3", "1.2.4", true), + (">= ^1.2.3", "1.9.3", true), + (">= ^1.2.3", "2.0.0", false), + (">= ^1.2.3", "1.2.2", false), + // this is considered the same as the above by node semver + ("> ^1.2.3", "1.2.3", true), + ("> ^1.2.3", "1.2.4", true), + ("> ^1.2.3", "1.9.3", true), + ("> ^1.2.3", "2.0.0", false), + ("> ^1.2.3", "1.2.2", false), + // this is also considered the same + ("< ^1.2.3", "1.2.3", true), + ("< ^1.2.3", "1.2.4", true), + ("< ^1.2.3", "1.9.3", true), + ("< ^1.2.3", "2.0.0", false), + ("< ^1.2.3", "1.2.2", false), + // same with this + ("<= ^1.2.3", "1.2.3", true), + ("<= ^1.2.3", "1.2.4", true), + ("<= ^1.2.3", "1.9.3", true), + ("<= ^1.2.3", "2.0.0", false), + ("<= ^1.2.3", "1.2.2", false), + // now try a ~, which should work the same as above, but for ~ + ("<= ~1.2.3", "1.2.3", true), + ("<= ~1.2.3", "1.2.4", true), + ("<= ~1.2.3", "1.9.3", false), + ("<= ~1.2.3", "2.0.0", false), + ("<= ~1.2.3", "1.2.2", false), + ]; + + for (req_text, version_text, satisfies) in fixtures { + let req = parse_npm_version_req(req_text).unwrap(); + let version = parse_npm_version(version_text).unwrap(); + assert_eq!( + req.matches(&version), + *satisfies, + "Checking {} {} satisfies {}", + req_text, + if *satisfies { "true" } else { "false" }, + version_text + ); + } + } + + #[test] + fn range_primitive_kind_beside_caret_or_tilde_no_whitespace() { + let fixtures = &[ + ">=^1.2.3", ">^1.2.3", "<^1.2.3", "<=^1.2.3", ">=~1.2.3", ">~1.2.3", + "<~1.2.3", "<=~1.2.3", + ]; + + for req_text in fixtures { + // when it has no space, this is considered invalid + // by node semver so we should error + assert!(parse_npm_version_req(req_text).is_err()); + } + } +} diff --git a/src/semver/range.rs b/src/semver/range.rs new file mode 100644 index 000000000..ab202b60e --- /dev/null +++ b/src/semver/range.rs @@ -0,0 +1,509 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::cmp::Ordering; + +use serde::Deserialize; +use serde::Serialize; + +use super::Version; + +/// Collection of ranges. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VersionRangeSet(pub Vec); + +impl VersionRangeSet { + pub fn satisfies(&self, version: &Version) -> bool { + self.0.iter().any(|r| r.satisfies(version)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RangeBound { + Version(VersionBound), + Unbounded, // matches everything +} + +impl RangeBound { + pub fn inclusive(version: Version) -> Self { + Self::version(VersionBoundKind::Inclusive, version) + } + + pub fn exclusive(version: Version) -> Self { + Self::version(VersionBoundKind::Exclusive, version) + } + + pub fn version(kind: VersionBoundKind, version: Version) -> Self { + Self::Version(VersionBound::new(kind, version)) + } + + pub fn clamp_start(&self, other: &RangeBound) -> RangeBound { + match &self { + RangeBound::Unbounded => other.clone(), + RangeBound::Version(self_bound) => RangeBound::Version(match &other { + RangeBound::Unbounded => self_bound.clone(), + RangeBound::Version(other_bound) => { + match self_bound.version.cmp(&other_bound.version) { + Ordering::Greater => self_bound.clone(), + Ordering::Less => other_bound.clone(), + Ordering::Equal => match self_bound.kind { + VersionBoundKind::Exclusive => self_bound.clone(), + VersionBoundKind::Inclusive => other_bound.clone(), + }, + } + } + }), + } + } + + pub fn clamp_end(&self, other: &RangeBound) -> RangeBound { + match &self { + RangeBound::Unbounded => other.clone(), + RangeBound::Version(self_bound) => { + RangeBound::Version(match other { + RangeBound::Unbounded => self_bound.clone(), + RangeBound::Version(other_bound) => { + match self_bound.version.cmp(&other_bound.version) { + // difference with above is the next two lines are switched + Ordering::Greater => other_bound.clone(), + Ordering::Less => self_bound.clone(), + Ordering::Equal => match self_bound.kind { + VersionBoundKind::Exclusive => self_bound.clone(), + VersionBoundKind::Inclusive => other_bound.clone(), + }, + } + } + }) + } + } + } + + pub fn has_pre_with_exact_major_minor_patch( + &self, + version: &Version, + ) -> bool { + if let RangeBound::Version(self_version) = &self { + if !self_version.version.pre.is_empty() + && self_version.version.major == version.major + && self_version.version.minor == version.minor + && self_version.version.patch == version.patch + { + return true; + } + } + false + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum VersionBoundKind { + Inclusive, + Exclusive, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VersionBound { + pub kind: VersionBoundKind, + pub version: Version, +} + +impl VersionBound { + pub fn new(kind: VersionBoundKind, version: Version) -> Self { + Self { kind, version } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VersionRange { + pub start: RangeBound, + pub end: RangeBound, +} + +impl VersionRange { + pub fn all() -> VersionRange { + VersionRange { + start: RangeBound::Version(VersionBound { + kind: VersionBoundKind::Inclusive, + version: Version::default(), + }), + end: RangeBound::Unbounded, + } + } + + pub fn none() -> VersionRange { + VersionRange { + start: RangeBound::Version(VersionBound { + kind: VersionBoundKind::Inclusive, + version: Version::default(), + }), + end: RangeBound::Version(VersionBound { + kind: VersionBoundKind::Exclusive, + version: Version::default(), + }), + } + } + + /// If this range won't match anything. + pub fn is_none(&self) -> bool { + if let RangeBound::Version(end) = &self.end { + end.kind == VersionBoundKind::Exclusive + && end.version.major == 0 + && end.version.minor == 0 + && end.version.patch == 0 + } else { + false + } + } + + pub fn satisfies(&self, version: &Version) -> bool { + let satisfies = self.min_satisfies(version) && self.max_satisfies(version); + if satisfies && !version.pre.is_empty() { + // check either side of the range has a pre and same version + self.start.has_pre_with_exact_major_minor_patch(version) + || self.end.has_pre_with_exact_major_minor_patch(version) + } else { + satisfies + } + } + + fn min_satisfies(&self, version: &Version) -> bool { + match &self.start { + RangeBound::Unbounded => true, + RangeBound::Version(bound) => match version.cmp(&bound.version) { + Ordering::Less => false, + Ordering::Equal => bound.kind == VersionBoundKind::Inclusive, + Ordering::Greater => true, + }, + } + } + + fn max_satisfies(&self, version: &Version) -> bool { + match &self.end { + RangeBound::Unbounded => true, + RangeBound::Version(bound) => match version.cmp(&bound.version) { + Ordering::Less => true, + Ordering::Equal => bound.kind == VersionBoundKind::Inclusive, + Ordering::Greater => false, + }, + } + } + + pub fn clamp(&self, range: &VersionRange) -> VersionRange { + let start = self.start.clamp_start(&range.start); + let end = self.end.clamp_end(&range.end); + // clamp the start range to the end when greater + let start = start.clamp_end(&end); + VersionRange { start, end } + } +} + +/// A range that could be a wildcard or number value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum XRange { + Wildcard, + Val(u64), +} + +/// A partial version. +#[derive(Debug, Clone)] +pub struct Partial { + pub major: XRange, + pub minor: XRange, + pub patch: XRange, + pub pre: Vec, + pub build: Vec, +} + +impl Partial { + pub fn as_tilde_version_range(&self) -> VersionRange { + // tilde ranges allow patch-level changes + let end = match self.major { + XRange::Wildcard => return VersionRange::all(), + XRange::Val(major) => match self.minor { + XRange::Wildcard => Version { + major: major + 1, + minor: 0, + patch: 0, + pre: Vec::new(), + build: Vec::new(), + }, + XRange::Val(minor) => Version { + major, + minor: minor + 1, + patch: 0, + pre: Vec::new(), + build: Vec::new(), + }, + }, + }; + VersionRange { + start: self.as_lower_bound(), + end: RangeBound::exclusive(end), + } + } + + pub fn as_caret_version_range(&self) -> VersionRange { + // partial ranges allow patch and minor updates, except when + // leading parts are < 1 in which case it will only bump the + // first non-zero or patch part + let end = match self.major { + XRange::Wildcard => return VersionRange::all(), + XRange::Val(major) => { + let next_major = Version { + major: major + 1, + ..Default::default() + }; + if major > 0 { + next_major + } else { + match self.minor { + XRange::Wildcard => next_major, + XRange::Val(minor) => { + let next_minor = Version { + minor: minor + 1, + ..Default::default() + }; + if minor > 0 { + next_minor + } else { + match self.patch { + XRange::Wildcard => next_minor, + XRange::Val(patch) => Version { + patch: patch + 1, + ..Default::default() + }, + } + } + } + } + } + } + }; + VersionRange { + start: self.as_lower_bound(), + end: RangeBound::Version(VersionBound { + kind: VersionBoundKind::Exclusive, + version: end, + }), + } + } + + pub fn as_lower_bound(&self) -> RangeBound { + RangeBound::inclusive(Version { + major: match self.major { + XRange::Val(val) => val, + XRange::Wildcard => 0, + }, + minor: match self.minor { + XRange::Val(val) => val, + XRange::Wildcard => 0, + }, + patch: match self.patch { + XRange::Val(val) => val, + XRange::Wildcard => 0, + }, + pre: self.pre.clone(), + build: self.build.clone(), + }) + } + + pub fn as_upper_bound(&self) -> RangeBound { + let mut end = Version::default(); + let mut kind = VersionBoundKind::Inclusive; + match self.patch { + XRange::Wildcard => { + end.minor += 1; + kind = VersionBoundKind::Exclusive; + } + XRange::Val(val) => { + end.patch = val; + } + } + match self.minor { + XRange::Wildcard => { + end.minor = 0; + end.major += 1; + kind = VersionBoundKind::Exclusive; + } + XRange::Val(val) => { + end.minor += val; + } + } + match self.major { + XRange::Wildcard => { + return RangeBound::Unbounded; + } + XRange::Val(val) => { + end.major += val; + } + } + + if kind == VersionBoundKind::Inclusive { + end.pre = self.pre.clone(); + } + + RangeBound::version(kind, end) + } + + pub fn as_equal_range(&self) -> VersionRange { + let major = match self.major { + XRange::Wildcard => { + return self.as_greater_range(VersionBoundKind::Inclusive) + } + XRange::Val(val) => val, + }; + let minor = match self.minor { + XRange::Wildcard => { + return self.as_greater_range(VersionBoundKind::Inclusive) + } + XRange::Val(val) => val, + }; + let patch = match self.patch { + XRange::Wildcard => { + return self.as_greater_range(VersionBoundKind::Inclusive) + } + XRange::Val(val) => val, + }; + let version = Version { + major, + minor, + patch, + pre: self.pre.clone(), + build: self.build.clone(), + }; + VersionRange { + start: RangeBound::inclusive(version.clone()), + end: RangeBound::inclusive(version), + } + } + + pub fn as_greater_than( + &self, + mut start_kind: VersionBoundKind, + ) -> VersionRange { + let major = match self.major { + XRange::Wildcard => match start_kind { + VersionBoundKind::Inclusive => return VersionRange::all(), + VersionBoundKind::Exclusive => return VersionRange::none(), + }, + XRange::Val(major) => major, + }; + let mut start = Version::default(); + + if start_kind == VersionBoundKind::Inclusive { + start.pre = self.pre.clone(); + } + + start.major = major; + match self.minor { + XRange::Wildcard => { + if start_kind == VersionBoundKind::Exclusive { + start_kind = VersionBoundKind::Inclusive; + start.major += 1; + } + } + XRange::Val(minor) => { + start.minor = minor; + } + } + match self.patch { + XRange::Wildcard => { + if start_kind == VersionBoundKind::Exclusive { + start_kind = VersionBoundKind::Inclusive; + start.minor += 1; + } + } + XRange::Val(patch) => { + start.patch = patch; + } + } + + VersionRange { + start: RangeBound::version(start_kind, start), + end: RangeBound::Unbounded, + } + } + + pub fn as_less_than(&self, mut end_kind: VersionBoundKind) -> VersionRange { + let major = match self.major { + XRange::Wildcard => match end_kind { + VersionBoundKind::Inclusive => return VersionRange::all(), + VersionBoundKind::Exclusive => return VersionRange::none(), + }, + XRange::Val(major) => major, + }; + let mut end = Version { + major, + ..Default::default() + }; + match self.minor { + XRange::Wildcard => { + if end_kind == VersionBoundKind::Inclusive { + end.major += 1; + } + end_kind = VersionBoundKind::Exclusive; + } + XRange::Val(minor) => { + end.minor = minor; + } + } + match self.patch { + XRange::Wildcard => { + if end_kind == VersionBoundKind::Inclusive { + end.minor += 1; + } + end_kind = VersionBoundKind::Exclusive; + } + XRange::Val(patch) => { + end.patch = patch; + } + } + if end_kind == VersionBoundKind::Inclusive { + end.pre = self.pre.clone(); + } + VersionRange { + start: RangeBound::Unbounded, + end: RangeBound::version(end_kind, end), + } + } + + pub fn as_greater_range(&self, start_kind: VersionBoundKind) -> VersionRange { + let major = match self.major { + XRange::Wildcard => return VersionRange::all(), + XRange::Val(major) => major, + }; + let mut start = Version::default(); + let mut end = Version::default(); + start.major = major; + end.major = major; + match self.patch { + XRange::Wildcard => { + if self.minor != XRange::Wildcard { + end.minor += 1; + } + } + XRange::Val(patch) => { + start.patch = patch; + end.patch = patch; + } + } + match self.minor { + XRange::Wildcard => { + end.major += 1; + } + XRange::Val(minor) => { + start.minor = minor; + end.minor += minor; + } + } + let end_kind = if start_kind == VersionBoundKind::Inclusive && start == end + { + VersionBoundKind::Inclusive + } else { + VersionBoundKind::Exclusive + }; + VersionRange { + start: RangeBound::version(start_kind, start), + end: RangeBound::version(end_kind, end), + } + } +} diff --git a/src/semver/specifier.rs b/src/semver/specifier.rs new file mode 100644 index 000000000..237ab44f2 --- /dev/null +++ b/src/semver/specifier.rs @@ -0,0 +1,269 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use monch::*; +use thiserror::Error; + +use super::range::Partial; +use super::range::VersionRange; +use super::range::VersionRangeSet; +use super::range::XRange; +use super::RangeSetOrTag; +use super::VersionReq; + +use super::is_valid_tag; + +#[derive(Error, Debug)] +#[error("Invalid npm specifier version requirement '{source}'.")] +pub struct NpmVersionReqSpecifierParseError { + #[source] + source: ParseErrorFailureError, +} + +pub fn parse_version_req_from_specifier( + text: &str, +) -> Result { + with_failure_handling(|input| { + map_res(version_range, |result| { + let (new_input, range_result) = match result { + Ok((input, range)) => (input, Ok(range)), + // use an empty string because we'll consider it a tag + Err(err) => ("", Err(err)), + }; + Ok(( + new_input, + VersionReq::from_raw_text_and_inner( + input.to_string(), + match range_result { + Ok(range) => RangeSetOrTag::RangeSet(VersionRangeSet(vec![range])), + Err(err) => { + if !is_valid_tag(input) { + return Err(err); + } else { + RangeSetOrTag::Tag(input.to_string()) + } + } + }, + ), + )) + })(input) + })(text) + .map_err(|err| NpmVersionReqSpecifierParseError { source: err }) +} + +// Note: Although the code below looks very similar to what's used for +// parsing npm version requirements, the code here is more strict +// in order to not allow for people to get ridiculous when using +// npm specifiers. +// +// A lot of the code below is adapted from https://github.com/npm/node-semver +// which is Copyright (c) Isaac Z. Schlueter and Contributors (ISC License) + +// version_range ::= partial | tilde | caret +fn version_range(input: &str) -> ParseResult { + or3( + map(preceded(ch('~'), partial), |partial| { + partial.as_tilde_version_range() + }), + map(preceded(ch('^'), partial), |partial| { + partial.as_caret_version_range() + }), + map(partial, |partial| partial.as_equal_range()), + )(input) +} + +// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? +fn partial(input: &str) -> ParseResult { + let (input, major) = xr()(input)?; + let (input, maybe_minor) = maybe(preceded(ch('.'), xr()))(input)?; + let (input, maybe_patch) = if maybe_minor.is_some() { + maybe(preceded(ch('.'), xr()))(input)? + } else { + (input, None) + }; + let (input, qual) = if maybe_patch.is_some() { + maybe(qualifier)(input)? + } else { + (input, None) + }; + let qual = qual.unwrap_or_default(); + Ok(( + input, + Partial { + major, + minor: maybe_minor.unwrap_or(XRange::Wildcard), + patch: maybe_patch.unwrap_or(XRange::Wildcard), + pre: qual.pre, + build: qual.build, + }, + )) +} + +// xr ::= 'x' | 'X' | '*' | nr +fn xr<'a>() -> impl Fn(&'a str) -> ParseResult<'a, XRange> { + or( + map(or3(tag("x"), tag("X"), tag("*")), |_| XRange::Wildcard), + map(nr, XRange::Val), + ) +} + +// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * +fn nr(input: &str) -> ParseResult { + or(map(tag("0"), |_| 0), move |input| { + let (input, result) = if_not_empty(substring(pair( + if_true(next_char, |c| c.is_ascii_digit() && *c != '0'), + skip_while(|c| c.is_ascii_digit()), + )))(input)?; + let val = match result.parse::() { + Ok(val) => val, + Err(err) => { + return ParseError::fail( + input, + format!("Error parsing '{result}' to u64.\n\n{err:#}"), + ) + } + }; + Ok((input, val)) + })(input) +} + +#[derive(Debug, Clone, Default)] +struct Qualifier { + pre: Vec, + build: Vec, +} + +// qualifier ::= ( '-' pre )? ( '+' build )? +fn qualifier(input: &str) -> ParseResult { + let (input, pre_parts) = maybe(pre)(input)?; + let (input, build_parts) = maybe(build)(input)?; + Ok(( + input, + Qualifier { + pre: pre_parts.unwrap_or_default(), + build: build_parts.unwrap_or_default(), + }, + )) +} + +// pre ::= parts +fn pre(input: &str) -> ParseResult> { + preceded(ch('-'), parts)(input) +} + +// build ::= parts +fn build(input: &str) -> ParseResult> { + preceded(ch('+'), parts)(input) +} + +// parts ::= part ( '.' part ) * +fn parts(input: &str) -> ParseResult> { + if_not_empty(map(separated_list(part, ch('.')), |text| { + text.into_iter().map(ToOwned::to_owned).collect() + }))(input) +} + +// part ::= nr | [-0-9A-Za-z]+ +fn part(input: &str) -> ParseResult<&str> { + // nr is in the other set, so don't bother checking for it + if_true( + take_while(|c| c.is_ascii_alphanumeric() || c == '-'), + |result| !result.is_empty(), + )(input) +} + +#[cfg(test)] +mod tests { + use super::super::Version; + use super::*; + + struct VersionReqTester(VersionReq); + + impl VersionReqTester { + fn new(text: &str) -> Self { + Self(parse_version_req_from_specifier(text).unwrap()) + } + + fn matches(&self, version: &str) -> bool { + self.0.matches(&Version::parse_from_npm(version).unwrap()) + } + } + + #[test] + fn version_req_exact() { + let tester = VersionReqTester::new("1.0.1"); + assert!(!tester.matches("1.0.0")); + assert!(tester.matches("1.0.1")); + assert!(!tester.matches("1.0.2")); + assert!(!tester.matches("1.1.1")); + + // pre-release + let tester = VersionReqTester::new("1.0.0-alpha.13"); + assert!(tester.matches("1.0.0-alpha.13")); + } + + #[test] + fn version_req_minor() { + let tester = VersionReqTester::new("1.1"); + assert!(!tester.matches("1.0.0")); + assert!(tester.matches("1.1.0")); + assert!(tester.matches("1.1.1")); + assert!(!tester.matches("1.2.0")); + assert!(!tester.matches("1.2.1")); + } + + #[test] + fn version_req_caret() { + let tester = VersionReqTester::new("^1.1.1"); + assert!(!tester.matches("1.1.0")); + assert!(tester.matches("1.1.1")); + assert!(tester.matches("1.1.2")); + assert!(tester.matches("1.2.0")); + assert!(!tester.matches("2.0.0")); + + let tester = VersionReqTester::new("^0.1.1"); + assert!(!tester.matches("0.0.0")); + assert!(!tester.matches("0.1.0")); + assert!(tester.matches("0.1.1")); + assert!(tester.matches("0.1.2")); + assert!(!tester.matches("0.2.0")); + assert!(!tester.matches("1.0.0")); + + let tester = VersionReqTester::new("^0.0.1"); + assert!(!tester.matches("0.0.0")); + assert!(tester.matches("0.0.1")); + assert!(!tester.matches("0.0.2")); + assert!(!tester.matches("0.1.0")); + assert!(!tester.matches("1.0.0")); + } + + #[test] + fn version_req_tilde() { + let tester = VersionReqTester::new("~1.1.1"); + assert!(!tester.matches("1.1.0")); + assert!(tester.matches("1.1.1")); + assert!(tester.matches("1.1.2")); + assert!(!tester.matches("1.2.0")); + assert!(!tester.matches("2.0.0")); + + let tester = VersionReqTester::new("~0.1.1"); + assert!(!tester.matches("0.0.0")); + assert!(!tester.matches("0.1.0")); + assert!(tester.matches("0.1.1")); + assert!(tester.matches("0.1.2")); + assert!(!tester.matches("0.2.0")); + assert!(!tester.matches("1.0.0")); + + let tester = VersionReqTester::new("~0.0.1"); + assert!(!tester.matches("0.0.0")); + assert!(tester.matches("0.0.1")); + assert!(tester.matches("0.0.2")); // for some reason this matches, but not with ^ + assert!(!tester.matches("0.1.0")); + assert!(!tester.matches("1.0.0")); + } + + #[test] + fn parses_tag() { + let latest_tag = VersionReq::parse_from_specifier("latest").unwrap(); + assert_eq!(latest_tag.tag().unwrap(), "latest"); + } +}