From 600fff79cdf5d52154344a0e3a8a523e1e21c3c1 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Tue, 31 Jan 2023 21:27:40 -0500 Subject: refactor(semver): generalize semver related structs (#17605) - Generalizes the npm version code (ex. `NpmVersion` -> `Version`, `NpmVersionReq` -> `VersionReq`). This is a slow refactor towards extracting out this code for deno specifiers and better usage in deno_graph. - Removes `SpecifierVersionReq`. Consolidates `NpmVersionReq` and `SpecifierVersionReq` to just `VersionReq` - Removes `NpmVersionMatcher`. This now just looks at `VersionReq`. - Paves the way to allow us to create `NpmPackageReference`'s from a package.json's dependencies/dev dependencies (`VersionReq::parse_from_npm`). --- cli/main.rs | 1 + cli/npm/cache.rs | 28 +- cli/npm/mod.rs | 3 - cli/npm/registry.rs | 15 +- cli/npm/resolution/graph.rs | 73 ++- cli/npm/resolution/mod.rs | 35 +- cli/npm/resolution/reference.rs | 298 ++++++++++ cli/npm/resolution/snapshot.rs | 14 +- cli/npm/resolution/specifier.rs | 310 +---------- cli/npm/semver/mod.rs | 1152 --------------------------------------- cli/npm/semver/range.rs | 509 ----------------- cli/npm/semver/specifier.rs | 299 ---------- cli/npm/tarball.rs | 9 +- cli/semver/mod.rs | 200 +++++++ cli/semver/npm.rs | 985 +++++++++++++++++++++++++++++++++ cli/semver/range.rs | 509 +++++++++++++++++ cli/semver/specifier.rs | 265 +++++++++ 17 files changed, 2348 insertions(+), 2357 deletions(-) create mode 100644 cli/npm/resolution/reference.rs delete mode 100644 cli/npm/semver/mod.rs delete mode 100644 cli/npm/semver/range.rs delete mode 100644 cli/npm/semver/specifier.rs create mode 100644 cli/semver/mod.rs create mode 100644 cli/semver/npm.rs create mode 100644 cli/semver/range.rs create mode 100644 cli/semver/specifier.rs (limited to 'cli') diff --git a/cli/main.rs b/cli/main.rs index 7504bf941..71e2c202b 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -18,6 +18,7 @@ mod npm; mod ops; mod proc_state; mod resolver; +mod semver; mod standalone; mod tools; mod tsc; diff --git a/cli/npm/cache.rs b/cli/npm/cache.rs index 0d07d27b2..888975926 100644 --- a/cli/npm/cache.rs +++ b/cli/npm/cache.rs @@ -17,13 +17,13 @@ use deno_core::url::Url; use crate::args::CacheSetting; use crate::cache::DenoDir; use crate::http_util::HttpClient; +use crate::semver::Version; use crate::util::fs::canonicalize_path; use crate::util::fs::hard_link_dir_recursive; use crate::util::path::root_url_to_safe_local_dirname; use crate::util::progress_bar::ProgressBar; use super::registry::NpmPackageVersionDistInfo; -use super::semver::NpmVersion; use super::tarball::verify_and_extract_tarball; /// For some of the tests, we want downloading of packages @@ -35,7 +35,7 @@ pub fn should_sync_download() -> bool { const NPM_PACKAGE_SYNC_LOCK_FILENAME: &str = ".deno_sync_lock"; pub fn with_folder_sync_lock( - package: (&str, &NpmVersion), + package: (&str, &Version), output_folder: &Path, action: impl FnOnce() -> Result<(), AnyError>, ) -> Result<(), AnyError> { @@ -108,7 +108,7 @@ pub fn with_folder_sync_lock( pub struct NpmPackageCacheFolderId { pub name: String, - pub version: NpmVersion, + pub version: Version, /// Peer dependency resolution may require us to have duplicate copies /// of the same package. pub copy_index: usize, @@ -202,7 +202,7 @@ impl ReadonlyNpmCache { pub fn package_folder_for_name_and_version( &self, name: &str, - version: &NpmVersion, + version: &Version, registry_url: &Url, ) -> PathBuf { self @@ -305,7 +305,7 @@ impl ReadonlyNpmCache { }; Some(NpmPackageCacheFolderId { name, - version: NpmVersion::parse(version).ok()?, + version: Version::parse_from_npm(version).ok()?, copy_index, }) } @@ -357,7 +357,7 @@ impl NpmCache { /// and imports a dynamic import that imports the same package again for example. fn should_use_global_cache_for_package( &self, - package: (&str, &NpmVersion), + package: (&str, &Version), ) -> bool { self.cache_setting.should_use_for_npm_package(package.0) || !self @@ -368,7 +368,7 @@ impl NpmCache { pub async fn ensure_package( &self, - package: (&str, &NpmVersion), + package: (&str, &Version), dist: &NpmPackageVersionDistInfo, registry_url: &Url, ) -> Result<(), AnyError> { @@ -382,7 +382,7 @@ impl NpmCache { async fn ensure_package_inner( &self, - package: (&str, &NpmVersion), + package: (&str, &Version), dist: &NpmPackageVersionDistInfo, registry_url: &Url, ) -> Result<(), AnyError> { @@ -467,7 +467,7 @@ impl NpmCache { pub fn package_folder_for_name_and_version( &self, name: &str, - version: &NpmVersion, + version: &Version, registry_url: &Url, ) -> PathBuf { self.readonly.package_folder_for_name_and_version( @@ -517,7 +517,7 @@ mod test { use super::ReadonlyNpmCache; use crate::npm::cache::NpmPackageCacheFolderId; - use crate::npm::semver::NpmVersion; + use crate::semver::Version; #[test] fn should_get_package_folder() { @@ -530,7 +530,7 @@ mod test { cache.package_folder_for_id( &NpmPackageCacheFolderId { name: "json".to_string(), - version: NpmVersion::parse("1.2.5").unwrap(), + version: Version::parse_from_npm("1.2.5").unwrap(), copy_index: 0, }, ®istry_url, @@ -545,7 +545,7 @@ mod test { cache.package_folder_for_id( &NpmPackageCacheFolderId { name: "json".to_string(), - version: NpmVersion::parse("1.2.5").unwrap(), + version: Version::parse_from_npm("1.2.5").unwrap(), copy_index: 1, }, ®istry_url, @@ -560,7 +560,7 @@ mod test { cache.package_folder_for_id( &NpmPackageCacheFolderId { name: "JSON".to_string(), - version: NpmVersion::parse("2.1.5").unwrap(), + version: Version::parse_from_npm("2.1.5").unwrap(), copy_index: 0, }, ®istry_url, @@ -575,7 +575,7 @@ mod test { cache.package_folder_for_id( &NpmPackageCacheFolderId { name: "@types/JSON".to_string(), - version: NpmVersion::parse("2.1.5").unwrap(), + version: Version::parse_from_npm("2.1.5").unwrap(), copy_index: 0, }, ®istry_url, diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index 9f41e508a..b9a4f493a 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -4,11 +4,8 @@ mod cache; mod registry; mod resolution; mod resolvers; -mod semver; mod tarball; -#[cfg(test)] -pub use self::semver::NpmVersion; pub use cache::NpmCache; #[cfg(test)] pub use registry::NpmPackageVersionDistInfo; diff --git a/cli/npm/registry.rs b/cli/npm/registry.rs index 9598feba1..fea6996ab 100644 --- a/cli/npm/registry.rs +++ b/cli/npm/registry.rs @@ -25,13 +25,12 @@ use serde::Serialize; use crate::args::CacheSetting; use crate::cache::CACHE_PERM; use crate::http_util::HttpClient; +use crate::semver::Version; +use crate::semver::VersionReq; use crate::util::fs::atomic_write_file; use crate::util::progress_bar::ProgressBar; use super::cache::NpmCache; -use super::resolution::NpmVersionMatcher; -use super::semver::NpmVersion; -use super::semver::NpmVersionReq; // npm registry docs: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md @@ -61,11 +60,11 @@ pub struct NpmDependencyEntry { pub kind: NpmDependencyEntryKind, pub bare_specifier: String, pub name: String, - pub version_req: NpmVersionReq, + pub version_req: VersionReq, /// When the dependency is also marked as a peer dependency, /// use this entry to resolve the dependency when it can't /// be resolved as a peer dependency. - pub peer_dep_version_req: Option, + pub peer_dep_version_req: Option, } impl PartialOrd for NpmDependencyEntry { @@ -82,7 +81,7 @@ impl Ord for NpmDependencyEntry { Ordering::Equal => other .version_req .version_text() - .cmp(&self.version_req.version_text()), + .cmp(self.version_req.version_text()), ordering => ordering, } } @@ -129,7 +128,7 @@ impl NpmPackageVersionInfo { (entry.0.clone(), entry.1.clone()) }; let version_req = - NpmVersionReq::parse(&version_req).with_context(|| { + VersionReq::parse_from_npm(&version_req).with_context(|| { format!( "error parsing version requirement for dependency: {bare_specifier}@{version_req}" ) @@ -217,7 +216,7 @@ pub trait NpmRegistryApi: Clone + Sync + Send + 'static { fn package_version_info( &self, name: &str, - version: &NpmVersion, + version: &Version, ) -> BoxFuture<'static, Result, AnyError>> { let api = self.clone(); let name = name.to_string(); diff --git a/cli/npm/resolution/graph.rs b/cli/npm/resolution/graph.rs index e21048149..20f192fbf 100644 --- a/cli/npm/resolution/graph.rs +++ b/cli/npm/resolution/graph.rs @@ -14,22 +14,25 @@ use deno_core::futures; use deno_core::parking_lot::Mutex; use deno_core::parking_lot::MutexGuard; use log::debug; +use once_cell::sync::Lazy; use crate::npm::cache::should_sync_download; use crate::npm::registry::NpmDependencyEntry; use crate::npm::registry::NpmDependencyEntryKind; use crate::npm::registry::NpmPackageInfo; use crate::npm::registry::NpmPackageVersionInfo; -use crate::npm::semver::NpmVersion; -use crate::npm::semver::NpmVersionReq; use crate::npm::NpmRegistryApi; +use crate::semver::Version; +use crate::semver::VersionReq; use super::snapshot::NpmResolutionSnapshot; use super::snapshot::SnapshotPackageCopyIndexResolver; use super::NpmPackageId; use super::NpmPackageReq; use super::NpmResolutionPackage; -use super::NpmVersionMatcher; + +pub static LATEST_VERSION_REQ: Lazy = + Lazy::new(|| VersionReq::parse_from_specifier("latest").unwrap()); /// A memory efficient path of visited name and versions in the graph /// which is used to detect cycles. @@ -419,11 +422,11 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> fn resolve_best_package_version_and_info<'info>( &self, - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, package_info: &'info NpmPackageInfo, ) -> Result, AnyError> { if let Some(version) = - self.resolve_best_package_version(package_info, version_matcher)? + self.resolve_best_package_version(package_info, version_req)? { match package_info.versions.get(&version.to_string()) { Some(version_info) => Ok(VersionAndInfo { @@ -440,20 +443,19 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> } } else { // get the information - get_resolved_package_version_and_info(version_matcher, package_info, None) + get_resolved_package_version_and_info(version_req, package_info, None) } } fn resolve_best_package_version( &self, package_info: &NpmPackageInfo, - version_matcher: &impl NpmVersionMatcher, - ) -> Result, AnyError> { - let mut maybe_best_version: Option<&NpmVersion> = None; + version_req: &VersionReq, + ) -> Result, AnyError> { + let mut maybe_best_version: Option<&Version> = None; if let Some(ids) = self.graph.packages_by_name.get(&package_info.name) { for version in ids.iter().map(|id| &id.version) { - if version_req_satisfies(version_matcher, version, package_info, None)? - { + if version_req_satisfies(version_req, version, package_info, None)? { let is_best_version = maybe_best_version .as_ref() .map(|best_version| (*best_version).cmp(version).is_lt()) @@ -478,7 +480,10 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> ) -> Result<(), AnyError> { let (_, node) = self.resolve_node_from_info( &package_req.name, - package_req, + package_req + .version_req + .as_ref() + .unwrap_or(&*LATEST_VERSION_REQ), package_info, None, )?; @@ -557,12 +562,12 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> fn resolve_node_from_info( &mut self, pkg_req_name: &str, - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, package_info: &NpmPackageInfo, parent_id: Option<&NpmPackageId>, ) -> Result<(NpmPackageId, Arc>), AnyError> { - let version_and_info = self - .resolve_best_package_version_and_info(version_matcher, package_info)?; + let version_and_info = + self.resolve_best_package_version_and_info(version_req, package_info)?; let id = NpmPackageId { name: package_info.name.to_string(), version: version_and_info.version.clone(), @@ -575,7 +580,7 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> None => "".to_string(), }, pkg_req_name, - version_matcher.version_text(), + version_req.version_text(), id.as_serialized(), ); let (created, node) = self.graph.get_or_create_for_id(&id); @@ -996,22 +1001,22 @@ impl<'a, TNpmRegistryApi: NpmRegistryApi> #[derive(Clone)] struct VersionAndInfo<'a> { - version: NpmVersion, + version: Version, info: &'a NpmPackageVersionInfo, } fn get_resolved_package_version_and_info<'a>( - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, info: &'a NpmPackageInfo, parent: Option<&NpmPackageId>, ) -> Result, AnyError> { - if let Some(tag) = version_matcher.tag() { + if let Some(tag) = version_req.tag() { tag_to_version_info(info, tag, parent) } else { let mut maybe_best_version: Option = None; for version_info in info.versions.values() { - let version = NpmVersion::parse(&version_info.version)?; - if version_matcher.matches(&version) { + let version = Version::parse_from_npm(&version_info.version)?; + if version_req.matches(&version) { let is_best_version = maybe_best_version .as_ref() .map(|best_version| best_version.version.cmp(&version).is_lt()) @@ -1042,7 +1047,7 @@ fn get_resolved_package_version_and_info<'a>( "Try retrieving the latest npm package information by running with --reload", ), info.name, - version_matcher.version_text(), + version_req.version_text(), match parent { Some(id) => format!(" as specified in {}", id.display()), None => String::new(), @@ -1053,17 +1058,17 @@ fn get_resolved_package_version_and_info<'a>( } fn version_req_satisfies( - matcher: &impl NpmVersionMatcher, - version: &NpmVersion, + version_req: &VersionReq, + version: &Version, package_info: &NpmPackageInfo, parent: Option<&NpmPackageId>, ) -> Result { - match matcher.tag() { + match version_req.tag() { Some(tag) => { let tag_version = tag_to_version_info(package_info, tag, parent)?.version; Ok(tag_version == *version) } - None => Ok(matcher.matches(version)), + None => Ok(version_req.matches(version)), } } @@ -1081,7 +1086,7 @@ fn tag_to_version_info<'a>( // explicit version. if tag == "latest" && info.name == "@types/node" { return get_resolved_package_version_and_info( - &NpmVersionReq::parse("18.0.0 - 18.11.18").unwrap(), + &VersionReq::parse_from_npm("18.0.0 - 18.11.18").unwrap(), info, parent, ); @@ -1090,7 +1095,7 @@ fn tag_to_version_info<'a>( if let Some(version) = info.dist_tags.get(tag) { match info.versions.get(version) { Some(info) => Ok(VersionAndInfo { - version: NpmVersion::parse(version)?, + version: Version::parse_from_npm(version)?, info, }), None => { @@ -1128,7 +1133,11 @@ mod test { )]), }; let result = get_resolved_package_version_and_info( - &package_ref.req, + package_ref + .req + .version_req + .as_ref() + .unwrap_or(&*LATEST_VERSION_REQ), &package_info, None, ); @@ -1157,7 +1166,11 @@ mod test { )]), }; let result = get_resolved_package_version_and_info( - &package_ref.req, + package_ref + .req + .version_req + .as_ref() + .unwrap_or(&*LATEST_VERSION_REQ), &package_info, None, ); diff --git a/cli/npm/resolution/mod.rs b/cli/npm/resolution/mod.rs index 407651ccb..990ad8d06 100644 --- a/cli/npm/resolution/mod.rs +++ b/cli/npm/resolution/mod.rs @@ -11,6 +11,7 @@ use serde::Deserialize; use serde::Serialize; use crate::args::Lockfile; +use crate::semver::Version; use self::graph::GraphDependencyResolver; use self::snapshot::NpmPackagesPartitioned; @@ -19,33 +20,25 @@ use super::cache::should_sync_download; use super::cache::NpmPackageCacheFolderId; use super::registry::NpmPackageVersionDistInfo; use super::registry::RealNpmRegistryApi; -use super::semver::NpmVersion; use super::NpmRegistryApi; mod graph; +mod reference; mod snapshot; mod specifier; use graph::Graph; +pub use reference::NpmPackageReference; +pub use reference::NpmPackageReq; pub use snapshot::NpmResolutionSnapshot; pub use specifier::resolve_graph_npm_info; -pub use specifier::NpmPackageReference; -pub use specifier::NpmPackageReq; - -/// The version matcher used for npm schemed urls is more strict than -/// the one used by npm packages and so we represent either via a trait. -pub trait NpmVersionMatcher { - fn tag(&self) -> Option<&str>; - fn matches(&self, version: &NpmVersion) -> bool; - fn version_text(&self) -> String; -} #[derive( Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize, )] pub struct NpmPackageId { pub name: String, - pub version: NpmVersion, + pub version: Version, pub peer_dependencies: Vec, } @@ -103,14 +96,12 @@ impl NpmPackageId { if_not_empty(substring(skip_while(|c| c != '_')))(input) } - fn parse_name_and_version( - input: &str, - ) -> ParseResult<(String, NpmVersion)> { + fn parse_name_and_version(input: &str) -> ParseResult<(String, Version)> { let (input, name) = parse_name(input)?; let (input, _) = ch('@')(input)?; let at_version_input = input; let (input, version) = parse_version(input)?; - match NpmVersion::parse(version) { + match Version::parse_from_npm(version) { Ok(version) => Ok((input, (name.to_string(), version))), Err(err) => ParseError::fail(at_version_input, format!("{err:#}")), } @@ -417,30 +408,30 @@ mod tests { fn serialize_npm_package_id() { let id = NpmPackageId { name: "pkg-a".to_string(), - version: NpmVersion::parse("1.2.3").unwrap(), + version: Version::parse_from_npm("1.2.3").unwrap(), peer_dependencies: vec![ NpmPackageId { name: "pkg-b".to_string(), - version: NpmVersion::parse("3.2.1").unwrap(), + version: Version::parse_from_npm("3.2.1").unwrap(), peer_dependencies: vec![ NpmPackageId { name: "pkg-c".to_string(), - version: NpmVersion::parse("1.3.2").unwrap(), + version: Version::parse_from_npm("1.3.2").unwrap(), peer_dependencies: vec![], }, NpmPackageId { name: "pkg-d".to_string(), - version: NpmVersion::parse("2.3.4").unwrap(), + version: Version::parse_from_npm("2.3.4").unwrap(), peer_dependencies: vec![], }, ], }, NpmPackageId { name: "pkg-e".to_string(), - version: NpmVersion::parse("2.3.1").unwrap(), + version: Version::parse_from_npm("2.3.1").unwrap(), peer_dependencies: vec![NpmPackageId { name: "pkg-f".to_string(), - version: NpmVersion::parse("2.3.1").unwrap(), + version: Version::parse_from_npm("2.3.1").unwrap(), peer_dependencies: vec![], }], }, diff --git a/cli/npm/resolution/reference.rs b/cli/npm/resolution/reference.rs new file mode 100644 index 000000000..2d34bcc34 --- /dev/null +++ b/cli/npm/resolution/reference.rs @@ -0,0 +1,298 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_ast::ModuleSpecifier; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use serde::Deserialize; +use serde::Serialize; + +use crate::semver::VersionReq; + +/// A reference to an npm package's name, version constraint, and potential sub path. +/// +/// This contains all the information found in an npm specifier. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct NpmPackageReference { + pub req: NpmPackageReq, + pub sub_path: Option, +} + +impl NpmPackageReference { + pub fn from_specifier( + specifier: &ModuleSpecifier, + ) -> Result { + Self::from_str(specifier.as_str()) + } + + pub fn from_str(specifier: &str) -> Result { + let original_text = specifier; + let specifier = match specifier.strip_prefix("npm:") { + Some(s) => { + // Strip leading slash, which might come from import map + s.strip_prefix('/').unwrap_or(s) + } + None => { + // don't allocate a string here and instead use a static string + // because this is hit a lot when a url is not an npm specifier + return Err(generic_error("Not an npm specifier")); + } + }; + let parts = specifier.split('/').collect::>(); + let name_part_len = if specifier.starts_with('@') { 2 } else { 1 }; + if parts.len() < name_part_len { + return Err(generic_error(format!("Not a valid package: {specifier}"))); + } + let name_parts = &parts[0..name_part_len]; + let req = match NpmPackageReq::parse_from_parts(name_parts) { + Ok(pkg_req) => pkg_req, + Err(err) => { + return Err(generic_error(format!( + "Invalid npm specifier '{original_text}'. {err:#}" + ))) + } + }; + let sub_path = if parts.len() == name_parts.len() { + None + } else { + let sub_path = parts[name_part_len..].join("/"); + if sub_path.is_empty() { + None + } else { + Some(sub_path) + } + }; + + if let Some(sub_path) = &sub_path { + if let Some(at_index) = sub_path.rfind('@') { + let (new_sub_path, version) = sub_path.split_at(at_index); + let msg = format!( + "Invalid package specifier 'npm:{req}/{sub_path}'. Did you mean to write 'npm:{req}{version}/{new_sub_path}'?" + ); + return Err(generic_error(msg)); + } + } + + Ok(NpmPackageReference { req, sub_path }) + } +} + +impl std::fmt::Display for NpmPackageReference { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(sub_path) = &self.sub_path { + write!(f, "npm:{}/{}", self.req, sub_path) + } else { + write!(f, "npm:{}", self.req) + } + } +} + +/// The name and version constraint component of an `NpmPackageReference`. +#[derive( + Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, +)] +pub struct NpmPackageReq { + pub name: String, + pub version_req: Option, +} + +impl std::fmt::Display for NpmPackageReq { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.version_req { + Some(req) => write!(f, "{}@{}", self.name, req), + None => write!(f, "{}", self.name), + } + } +} + +impl NpmPackageReq { + pub fn from_str(text: &str) -> Result { + let parts = text.split('/').collect::>(); + match NpmPackageReq::parse_from_parts(&parts) { + Ok(req) => Ok(req), + Err(err) => { + let msg = format!("Invalid npm package requirement '{text}'. {err:#}"); + Err(generic_error(msg)) + } + } + } + + fn parse_from_parts(name_parts: &[&str]) -> Result { + assert!(!name_parts.is_empty()); // this should be provided the result of a string split + let last_name_part = &name_parts[name_parts.len() - 1]; + let (name, version_req) = if let Some(at_index) = last_name_part.rfind('@') + { + let version = &last_name_part[at_index + 1..]; + let last_name_part = &last_name_part[..at_index]; + let version_req = VersionReq::parse_from_specifier(version) + .with_context(|| "Invalid version requirement.")?; + let name = if name_parts.len() == 1 { + last_name_part.to_string() + } else { + format!("{}/{}", name_parts[0], last_name_part) + }; + (name, Some(version_req)) + } else { + (name_parts.join("/"), None) + }; + if name.is_empty() { + bail!("Did not contain a package name.") + } + Ok(Self { name, version_req }) + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn parse_npm_package_ref() { + assert_eq!( + NpmPackageReference::from_str("npm:@package/test").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "@package/test".to_string(), + version_req: None, + }, + sub_path: None, + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:@package/test@1").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "@package/test".to_string(), + version_req: Some(VersionReq::parse_from_specifier("1").unwrap()), + }, + sub_path: None, + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:@package/test@~1.1/sub_path").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "@package/test".to_string(), + version_req: Some(VersionReq::parse_from_specifier("~1.1").unwrap()), + }, + sub_path: Some("sub_path".to_string()), + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:@package/test/sub_path").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "@package/test".to_string(), + version_req: None, + }, + sub_path: Some("sub_path".to_string()), + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:test").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "test".to_string(), + version_req: None, + }, + sub_path: None, + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:test@^1.2").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "test".to_string(), + version_req: Some(VersionReq::parse_from_specifier("^1.2").unwrap()), + }, + sub_path: None, + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:test@~1.1/sub_path").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "test".to_string(), + version_req: Some(VersionReq::parse_from_specifier("~1.1").unwrap()), + }, + sub_path: Some("sub_path".to_string()), + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:@package/test/sub_path").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "@package/test".to_string(), + version_req: None, + }, + sub_path: Some("sub_path".to_string()), + } + ); + + assert_eq!( + NpmPackageReference::from_str("npm:@package") + .err() + .unwrap() + .to_string(), + "Not a valid package: @package" + ); + + // should parse leading slash + assert_eq!( + NpmPackageReference::from_str("npm:/@package/test/sub_path").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "@package/test".to_string(), + version_req: None, + }, + sub_path: Some("sub_path".to_string()), + } + ); + assert_eq!( + NpmPackageReference::from_str("npm:/test").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "test".to_string(), + version_req: None, + }, + sub_path: None, + } + ); + assert_eq!( + NpmPackageReference::from_str("npm:/test/").unwrap(), + NpmPackageReference { + req: NpmPackageReq { + name: "test".to_string(), + version_req: None, + }, + sub_path: None, + } + ); + + // should error for no name + assert_eq!( + NpmPackageReference::from_str("npm:/") + .err() + .unwrap() + .to_string(), + "Invalid npm specifier 'npm:/'. Did not contain a package name." + ); + assert_eq!( + NpmPackageReference::from_str("npm://test") + .err() + .unwrap() + .to_string(), + "Invalid npm specifier 'npm://test'. Did not contain a package name." + ); + } +} diff --git a/cli/npm/resolution/snapshot.rs b/cli/npm/resolution/snapshot.rs index be64ea611..934320a1d 100644 --- a/cli/npm/resolution/snapshot.rs +++ b/cli/npm/resolution/snapshot.rs @@ -19,11 +19,11 @@ use crate::npm::cache::NpmPackageCacheFolderId; use crate::npm::registry::NpmPackageVersionDistInfo; use crate::npm::registry::NpmRegistryApi; use crate::npm::registry::RealNpmRegistryApi; +use crate::semver::VersionReq; use super::NpmPackageId; use super::NpmPackageReq; use super::NpmResolutionPackage; -use super::NpmVersionMatcher; /// Packages partitioned by if they are "copy" packages or not. pub struct NpmPackagesPartitioned { @@ -159,12 +159,8 @@ impl NpmResolutionSnapshot { // TODO(bartlomieju): this should use a reverse lookup table in the // snapshot instead of resolving best version again. - let req = NpmPackageReq { - name: name.to_string(), - version_req: None, - }; - - if let Some(id) = self.resolve_best_package_id(name, &req) { + let any_version_req = VersionReq::parse_from_npm("*").unwrap(); + if let Some(id) = self.resolve_best_package_id(name, &any_version_req) { if let Some(pkg) = self.packages.get(&id) { return Ok(pkg); } @@ -201,14 +197,14 @@ impl NpmResolutionSnapshot { pub fn resolve_best_package_id( &self, name: &str, - version_matcher: &impl NpmVersionMatcher, + version_req: &VersionReq, ) -> Option { // todo(dsherret): this is not exactly correct because some ids // will be better than others due to peer dependencies let mut maybe_best_id: Option<&NpmPackageId> = None; if let Some(ids) = self.packages_by_name.get(name) { for id in ids { - if version_matcher.matches(&id.version) { + if version_req.matches(&id.version) { let is_best_version = maybe_best_id .as_ref() .map(|best_id| best_id.version.cmp(&id.version).is_lt()) diff --git a/cli/npm/resolution/specifier.rs b/cli/npm/resolution/specifier.rs index 0aa693472..78d313412 100644 --- a/cli/npm/resolution/specifier.rs +++ b/cli/npm/resolution/specifier.rs @@ -6,165 +6,13 @@ use std::collections::HashSet; use std::collections::VecDeque; use deno_ast::ModuleSpecifier; -use deno_core::anyhow::Context; -use deno_core::error::generic_error; -use deno_core::error::AnyError; use deno_graph::ModuleGraph; use deno_graph::Resolved; -use serde::Deserialize; -use serde::Serialize; -use super::super::semver::NpmVersion; -use super::super::semver::SpecifierVersionReq; -use super::NpmVersionMatcher; +use crate::semver::VersionReq; -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct NpmPackageReference { - pub req: NpmPackageReq, - pub sub_path: Option, -} - -impl NpmPackageReference { - pub fn from_specifier( - specifier: &ModuleSpecifier, - ) -> Result { - Self::from_str(specifier.as_str()) - } - - pub fn from_str(specifier: &str) -> Result { - let original_text = specifier; - let specifier = match specifier.strip_prefix("npm:") { - Some(s) => { - // Strip leading slash, which might come from import map - s.strip_prefix('/').unwrap_or(s) - } - None => { - // don't allocate a string here and instead use a static string - // because this is hit a lot when a url is not an npm specifier - return Err(generic_error("Not an npm specifier")); - } - }; - let parts = specifier.split('/').collect::>(); - let name_part_len = if specifier.starts_with('@') { 2 } else { 1 }; - if parts.len() < name_part_len { - return Err(generic_error(format!("Not a valid package: {specifier}"))); - } - let name_parts = &parts[0..name_part_len]; - let last_name_part = &name_parts[name_part_len - 1]; - let (name, version_req) = if let Some(at_index) = last_name_part.rfind('@') - { - let version = &last_name_part[at_index + 1..]; - let last_name_part = &last_name_part[..at_index]; - let version_req = SpecifierVersionReq::parse(version) - .with_context(|| "Invalid version requirement.")?; - let name = if name_part_len == 1 { - last_name_part.to_string() - } else { - format!("{}/{}", name_parts[0], last_name_part) - }; - (name, Some(version_req)) - } else { - (name_parts.join("/"), None) - }; - let sub_path = if parts.len() == name_parts.len() { - None - } else { - let sub_path = parts[name_part_len..].join("/"); - if sub_path.is_empty() { - None - } else { - Some(sub_path) - } - }; - - if let Some(sub_path) = &sub_path { - if let Some(at_index) = sub_path.rfind('@') { - let (new_sub_path, version) = sub_path.split_at(at_index); - let msg = format!( - "Invalid package specifier 'npm:{name}/{sub_path}'. Did you mean to write 'npm:{name}{version}/{new_sub_path}'?" - ); - return Err(generic_error(msg)); - } - } - - if name.is_empty() { - let msg = format!( - "Invalid npm specifier '{original_text}'. Did not contain a package name." - ); - return Err(generic_error(msg)); - } - - Ok(NpmPackageReference { - req: NpmPackageReq { name, version_req }, - sub_path, - }) - } -} - -impl std::fmt::Display for NpmPackageReference { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(sub_path) = &self.sub_path { - write!(f, "npm:{}/{}", self.req, sub_path) - } else { - write!(f, "npm:{}", self.req) - } - } -} - -#[derive( - Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, -)] -pub struct NpmPackageReq { - pub name: String, - pub version_req: Option, -} - -impl std::fmt::Display for NpmPackageReq { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.version_req { - Some(req) => write!(f, "{}@{}", self.name, req), - None => write!(f, "{}", self.name), - } - } -} - -impl NpmPackageReq { - pub fn from_str(text: &str) -> Result { - // probably should do something more targeted in the future - let reference = NpmPackageReference::from_str(&format!("npm:{text}"))?; - Ok(reference.req) - } -} - -impl NpmVersionMatcher for NpmPackageReq { - fn tag(&self) -> Option<&str> { - match &self.version_req { - Some(version_req) => version_req.tag(), - None => Some("latest"), - } - } - - fn matches(&self, version: &NpmVersion) -> bool { - match self.version_req.as_ref() { - Some(req) => { - assert_eq!(self.tag(), None); - match req.range() { - Some(range) => range.satisfies(version), - None => false, - } - } - None => version.pre.is_empty(), - } - } - - fn version_text(&self) -> String { - self - .version_req - .as_ref() - .map(|v| format!("{v}")) - .unwrap_or_else(|| "non-prerelease".to_string()) - } -} +use super::NpmPackageReference; +use super::NpmPackageReq; pub struct GraphNpmInfo { /// The order of these package requirements is the order they @@ -537,10 +385,7 @@ fn cmp_folder_specifiers(a: &ModuleSpecifier, b: &ModuleSpecifier) -> Ordering { // duplicate packages (so sort None last since it's `*`), but // mostly to create some determinism around how these are resolved. fn cmp_package_req(a: &NpmPackageReq, b: &NpmPackageReq) -> Ordering { - fn cmp_specifier_version_req( - a: &SpecifierVersionReq, - b: &SpecifierVersionReq, - ) -> Ordering { + fn cmp_specifier_version_req(a: &VersionReq, b: &VersionReq) -> Ordering { match a.tag() { Some(a_tag) => match b.tag() { Some(b_tag) => b_tag.cmp(a_tag), // sort descending @@ -581,153 +426,6 @@ mod tests { use super::*; - #[test] - fn parse_npm_package_ref() { - assert_eq!( - NpmPackageReference::from_str("npm:@package/test").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: None, - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test@1").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: Some(SpecifierVersionReq::parse("1").unwrap()), - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test@~1.1/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: Some(SpecifierVersionReq::parse("~1.1").unwrap()), - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: None, - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:test").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: None, - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:test@^1.2").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: Some(SpecifierVersionReq::parse("^1.2").unwrap()), - }, - sub_path: None, - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:test@~1.1/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: Some(SpecifierVersionReq::parse("~1.1").unwrap()), - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package/test/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: None, - }, - sub_path: Some("sub_path".to_string()), - } - ); - - assert_eq!( - NpmPackageReference::from_str("npm:@package") - .err() - .unwrap() - .to_string(), - "Not a valid package: @package" - ); - - // should parse leading slash - assert_eq!( - NpmPackageReference::from_str("npm:/@package/test/sub_path").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "@package/test".to_string(), - version_req: None, - }, - sub_path: Some("sub_path".to_string()), - } - ); - assert_eq!( - NpmPackageReference::from_str("npm:/test").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: None, - }, - sub_path: None, - } - ); - assert_eq!( - NpmPackageReference::from_str("npm:/test/").unwrap(), - NpmPackageReference { - req: NpmPackageReq { - name: "test".to_string(), - version_req: None, - }, - sub_path: None, - } - ); - - // should error for no name - assert_eq!( - NpmPackageReference::from_str("npm:/") - .err() - .unwrap() - .to_string(), - "Invalid npm specifier 'npm:/'. Did not contain a package name." - ); - assert_eq!( - NpmPackageReference::from_str("npm://test") - .err() - .unwrap() - .to_string(), - "Invalid npm specifier 'npm://test'. Did not contain a package name." - ); - } - #[test] fn sorting_folder_specifiers() { fn cmp(a: &str, b: &str) -> Ordering { diff --git a/cli/npm/semver/mod.rs b/cli/npm/semver/mod.rs deleted file mode 100644 index b532835e6..000000000 --- a/cli/npm/semver/mod.rs +++ /dev/null @@ -1,1152 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::cmp::Ordering; -use std::fmt; - -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use monch::*; -use serde::Deserialize; -use serde::Serialize; - -use crate::npm::resolution::NpmVersionMatcher; - -use self::range::Partial; -use self::range::VersionBoundKind; -use self::range::VersionRange; -use self::range::VersionRangeSet; - -use self::range::XRange; -pub use self::specifier::SpecifierVersionReq; - -mod range; -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, -)] -pub struct NpmVersion { - pub major: u64, - pub minor: u64, - pub patch: u64, - pub pre: Vec, - pub build: Vec, -} - -impl fmt::Display for NpmVersion { - 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 NpmVersion { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl std::cmp::Ord for NpmVersion { - 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; - } - } - } -} - -impl NpmVersion { - pub fn parse(text: &str) -> Result { - let text = text.trim(); - with_failure_handling(parse_npm_version)(text) - .with_context(|| format!("Invalid npm version '{text}'.")) - } -} - -fn parse_npm_version(input: &str) -> ParseResult { - 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, - NpmVersion { - major, - minor, - patch, - pre: q.pre, - build: q.build, - }, - )) -} - -#[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, - inner: NpmVersionReqInner, -} - -impl NpmVersionMatcher for NpmVersionReq { - fn tag(&self) -> Option<&str> { - match &self.inner { - NpmVersionReqInner::RangeSet(_) => None, - NpmVersionReqInner::Tag(tag) => Some(tag.as_str()), - } - } - - fn matches(&self, version: &NpmVersion) -> bool { - 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 { - self.raw_text.clone() - } -} - -impl fmt::Display for NpmVersionReq { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", &self.raw_text) - } -} - -impl NpmVersionReq { - pub fn parse(text: &str) -> Result { - let text = text.trim(); - with_failure_handling(parse_npm_version_req)(text) - .with_context(|| format!("Invalid npm version requirement '{text}'.")) - } -} - -fn parse_npm_version_req(input: &str) -> ParseResult { - map(inner, |inner| NpmVersionReq { - raw_text: input.to_string(), - inner, - })(input) -} - -// 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, - 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::>(); - Ok((input, NpmVersionReqInner::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(NpmVersionReq); - - impl NpmVersionReqTester { - fn new(text: &str) -> Self { - Self(NpmVersionReq::parse(text).unwrap()) - } - - fn matches(&self, version: &str) -> bool { - self.0.matches(&NpmVersion::parse(version).unwrap()) - } - } - - #[test] - pub fn npm_version_req_with_v() { - assert!(NpmVersionReq::parse("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 = 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!( - $a.cmp(&$b), - $expected, - "expected {} to be {:?} {}", - $a, - $expected, - $b - ); - }; - } - - macro_rules! test_compare { - ($a:expr, $b:expr, $expected:expr) => { - let a = NpmVersion::parse($a).unwrap(); - let b = NpmVersion::parse($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 = NpmVersion::parse(a).unwrap(); - let b = NpmVersion::parse(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 = NpmVersionReq::parse(range_text).unwrap(); - let expected_range = NpmVersionReq::parse(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 = NpmVersionReq::parse(req_text).unwrap(); - let version = NpmVersion::parse(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 = NpmVersionReq::parse(req_text).unwrap(); - let version = NpmVersion::parse(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 = NpmVersionReq::parse(req_text).unwrap(); - let version = NpmVersion::parse(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!(NpmVersionReq::parse(req_text).is_err()); - } - } -} diff --git a/cli/npm/semver/range.rs b/cli/npm/semver/range.rs deleted file mode 100644 index 07ee2d62a..000000000 --- a/cli/npm/semver/range.rs +++ /dev/null @@ -1,509 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::cmp::Ordering; - -use serde::Deserialize; -use serde::Serialize; - -use super::NpmVersion; - -/// Collection of ranges. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct VersionRangeSet(pub Vec); - -impl VersionRangeSet { - pub fn satisfies(&self, version: &NpmVersion) -> 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: NpmVersion) -> Self { - Self::version(VersionBoundKind::Inclusive, version) - } - - pub fn exclusive(version: NpmVersion) -> Self { - Self::version(VersionBoundKind::Exclusive, version) - } - - pub fn version(kind: VersionBoundKind, version: NpmVersion) -> 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: &NpmVersion, - ) -> 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: NpmVersion, -} - -impl VersionBound { - pub fn new(kind: VersionBoundKind, version: NpmVersion) -> 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: NpmVersion::default(), - }), - end: RangeBound::Unbounded, - } - } - - pub fn none() -> VersionRange { - VersionRange { - start: RangeBound::Version(VersionBound { - kind: VersionBoundKind::Inclusive, - version: NpmVersion::default(), - }), - end: RangeBound::Version(VersionBound { - kind: VersionBoundKind::Exclusive, - version: NpmVersion::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: &NpmVersion) -> 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: &NpmVersion) -> 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: &NpmVersion) -> 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 => NpmVersion { - major: major + 1, - minor: 0, - patch: 0, - pre: Vec::new(), - build: Vec::new(), - }, - XRange::Val(minor) => NpmVersion { - 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 = NpmVersion { - major: major + 1, - ..Default::default() - }; - if major > 0 { - next_major - } else { - match self.minor { - XRange::Wildcard => next_major, - XRange::Val(minor) => { - let next_minor = NpmVersion { - minor: minor + 1, - ..Default::default() - }; - if minor > 0 { - next_minor - } else { - match self.patch { - XRange::Wildcard => next_minor, - XRange::Val(patch) => NpmVersion { - 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(NpmVersion { - 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 = NpmVersion::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 = NpmVersion { - 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 = NpmVersion::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 = NpmVersion { - 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 = NpmVersion::default(); - let mut end = NpmVersion::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/cli/npm/semver/specifier.rs b/cli/npm/semver/specifier.rs deleted file mode 100644 index b12a5c308..000000000 --- a/cli/npm/semver/specifier.rs +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use monch::*; -use serde::Deserialize; -use serde::Serialize; - -use super::is_valid_npm_tag; -use super::range::Partial; -use super::range::VersionRange; -use super::range::XRange; - -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -enum SpecifierVersionReqInner { - Range(VersionRange), - Tag(String), -} - -/// Version requirement found in npm specifiers. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct SpecifierVersionReq { - raw_text: String, - inner: SpecifierVersionReqInner, -} - -impl std::fmt::Display for SpecifierVersionReq { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.raw_text) - } -} - -impl SpecifierVersionReq { - pub fn parse(text: &str) -> Result { - with_failure_handling(parse_npm_specifier)(text).with_context(|| { - format!("Invalid npm specifier version requirement '{text}'.") - }) - } - - pub fn range(&self) -> Option<&VersionRange> { - match &self.inner { - SpecifierVersionReqInner::Range(range) => Some(range), - SpecifierVersionReqInner::Tag(_) => None, - } - } - - pub fn tag(&self) -> Option<&str> { - match &self.inner { - SpecifierVersionReqInner::Range(_) => None, - SpecifierVersionReqInner::Tag(tag) => Some(tag.as_str()), - } - } -} - -fn parse_npm_specifier(input: &str) -> ParseResult { - 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, - SpecifierVersionReq { - raw_text: input.to_string(), - inner: match range_result { - Ok(range) => SpecifierVersionReqInner::Range(range), - Err(err) => { - if !is_valid_npm_tag(input) { - return Err(err); - } else { - SpecifierVersionReqInner::Tag(input.to_string()) - } - } - }, - }, - )) - })(input) -} - -// 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. - -// 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 crate::npm::semver::NpmVersion; - - use super::*; - - struct VersionReqTester(SpecifierVersionReq); - - impl VersionReqTester { - fn new(text: &str) -> Self { - Self(SpecifierVersionReq::parse(text).unwrap()) - } - - fn matches(&self, version: &str) -> bool { - self - .0 - .range() - .map(|r| r.satisfies(&NpmVersion::parse(version).unwrap())) - .unwrap_or(false) - } - } - - #[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 = SpecifierVersionReq::parse("latest").unwrap(); - assert_eq!(latest_tag.tag().unwrap(), "latest"); - } -} diff --git a/cli/npm/tarball.rs b/cli/npm/tarball.rs index 758ac3ded..3abf4f12f 100644 --- a/cli/npm/tarball.rs +++ b/cli/npm/tarball.rs @@ -13,10 +13,10 @@ use tar::EntryType; use super::cache::with_folder_sync_lock; use super::registry::NpmPackageVersionDistInfo; -use super::semver::NpmVersion; +use crate::semver::Version; pub fn verify_and_extract_tarball( - package: (&str, &NpmVersion), + package: (&str, &Version), data: &[u8], dist_info: &NpmPackageVersionDistInfo, output_folder: &Path, @@ -29,7 +29,7 @@ pub fn verify_and_extract_tarball( } fn verify_tarball_integrity( - package: (&str, &NpmVersion), + package: (&str, &Version), data: &[u8], npm_integrity: &str, ) -> Result<(), AnyError> { @@ -120,12 +120,11 @@ fn extract_tarball(data: &[u8], output_folder: &Path) -> Result<(), AnyError> { #[cfg(test)] mod test { use super::*; - use crate::npm::semver::NpmVersion; #[test] pub fn test_verify_tarball() { let package_name = "package".to_string(); - let package_version = NpmVersion::parse("1.0.0").unwrap(); + let package_version = Version::parse_from_npm("1.0.0").unwrap(); let package = (package_name.as_str(), &package_version); let actual_checksum = "z4phnx7vul3xvchq1m2ab9yg5aulvxxcg/spidns6c5h0ne8xyxysp+dgnkhfuwvy7kxvudbeoglodj6+sfapg=="; diff --git a/cli/semver/mod.rs b/cli/semver/mod.rs new file mode 100644 index 000000000..4fedddf5e --- /dev/null +++ b/cli/semver/mod.rs @@ -0,0 +1,200 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::cmp::Ordering; +use std::fmt; + +use deno_core::error::AnyError; +use serde::Deserialize; +use serde::Serialize; + +mod npm; +mod range; +mod specifier; + +use self::npm::parse_npm_version_req; +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; + +#[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 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/cli/semver/npm.rs b/cli/semver/npm.rs new file mode 100644 index 000000000..d95861b2c --- /dev/null +++ b/cli/semver/npm.rs @@ -0,0 +1,985 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use monch::*; + +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) + +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) + .with_context(|| format!("Invalid npm version '{text}'.")) +} + +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) + .with_context(|| format!("Invalid npm version requirement '{text}'.")) +} + +// 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/cli/semver/range.rs b/cli/semver/range.rs new file mode 100644 index 000000000..ab202b60e --- /dev/null +++ b/cli/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/cli/semver/specifier.rs b/cli/semver/specifier.rs new file mode 100644 index 000000000..8edb4cddd --- /dev/null +++ b/cli/semver/specifier.rs @@ -0,0 +1,265 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use monch::*; + +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; + +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) + .with_context(|| { + format!("Invalid npm specifier version requirement '{text}'.") + }) +} + +// 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"); + } +} -- cgit v1.2.3