diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b932f7f..ae89144c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ - [Rust](https://github.com/moonrepo/rust-plugin/blob/master/CHANGELOG.md) - [Schema](https://github.com/moonrepo/schema-plugin/blob/master/CHANGELOG.md) +## Unreleased + +#### 🐞 Fixes + +- Another attempt at fixing WASM memory issues. +- Fixed an issue where binaries sometimes could not be located for "installed" tools. + ## 0.18.2 #### 🐞 Fixes diff --git a/Cargo.lock b/Cargo.lock index f7e565059..93cbb9ee2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1142,9 +1142,9 @@ dependencies = [ [[package]] name = "extism" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25944ee40692db74f53a65804c08f23b88c4f2b0903f3098e94bfcf6cbfd3d0f" +checksum = "7a94848d5b49906bd97b83cf5a8bd25082dbc6f8bdfe98f12687910228734552" dependencies = [ "anyhow", "extism-manifest", @@ -1192,9 +1192,9 @@ dependencies = [ [[package]] name = "extism-runtime" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b284fa52d9c9eed7c26bfaa348183f1c83f08e080859a392b481c70f3c0f848e" +checksum = "a3b0ba8ef6ecbf59c0f6e47fd2feea575ebc3a09e81603d06a41af92fe61cdfa" dependencies = [ "anyhow", "cbindgen", @@ -2416,7 +2416,6 @@ dependencies = [ "proto_wasm_plugin", "regex", "reqwest", - "schematic", "semver", "serde", "serde_json", @@ -2430,6 +2429,7 @@ dependencies = [ "tinytemplate", "tracing", "url", + "version_spec", "warpgate", ] @@ -2862,18 +2862,16 @@ dependencies = [ [[package]] name = "schematic" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8592df69240651bac26079f142ab7c796ed888f7b5c0c4cddb5efc34149ca79b" +checksum = "fe561cb5c8380d36ef80bf81a7cc16acfde8a0d130dd3394cc0cb48809d065ab" dependencies = [ "garde", "indexmap 2.0.0", "miette", - "reqwest", "schematic_macros", "schematic_types", "serde", - "serde_json", "serde_path_to_error", "starbase_styles", "thiserror", @@ -3831,6 +3829,18 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "version_spec" +version = "0.1.0" +dependencies = [ + "human-sort", + "once_cell", + "regex", + "schematic", + "semver", + "serde", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 0e15b74b4..79f351d2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,7 @@ miette = "5.10.0" once_cell = "1.18.0" once_map = "0.4.8" regex = "1.9.5" -reqwest = { version = "0.11.20", default-features = false, features = [ - "rustls-tls-native-roots", -] } +reqwest = { version = "0.11.20", default-features = false } semver = "1.0.18" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 167d6758e..1e24753d2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -40,7 +40,7 @@ futures = "0.3.28" human-sort = { workspace = true } indicatif = "0.17.6" miette = { workspace = true } -reqwest = { workspace = true, features = ["stream"] } +reqwest = { workspace = true, features = ["rustls-tls-native-roots", "stream"] } semver = { workspace = true } serde = { workspace = true } starbase = "0.2.6" diff --git a/crates/cli/src/commands/clean.rs b/crates/cli/src/commands/clean.rs index ed538a85e..3f08f6e6a 100644 --- a/crates/cli/src/commands/clean.rs +++ b/crates/cli/src/commands/clean.rs @@ -1,7 +1,7 @@ use clap::Args; use dialoguer::Confirm; use proto_core::{ - get_plugins_dir, get_shim_file_name, load_tool, Id, Tool, ToolsConfig, VersionSpec, + get_plugins_dir, get_shim_file_name, load_tool, Id, ProtoError, Tool, ToolsConfig, VersionSpec, }; use proto_pdk_api::{CreateShimsInput, CreateShimsOutput}; use starbase::diagnostics::IntoDiagnostic; @@ -70,7 +70,10 @@ pub async fn clean_tool(mut tool: Tool, now: u128, days: u8, yes: bool) -> miett continue; } - let version = VersionSpec::parse(&dir_name)?; + let version = VersionSpec::parse(&dir_name).map_err(|error| ProtoError::Semver { + version: dir_name, + error, + })?; if !tool.manifest.versions.contains_key(&version) { debug!( diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d185a0c52..17ed33027 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/moonrepo/proto" [dependencies] proto_pdk_api = { version = "0.7.2", path = "../pdk-api" } proto_wasm_plugin = { version = "0.6.6", path = "../wasm-plugin" } +version_spec = { version = "0.1.0", path = "../version-spec" } warpgate = { version = "0.5.7", path = "../warpgate" } cached = { workspace = true } extism = { workspace = true } @@ -18,9 +19,6 @@ miette = { workspace = true } once_cell = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } -schematic = { version = "0.11.6", default-features = false, features = [ - "json", -], optional = true } semver = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -36,7 +34,3 @@ url = "2.4.1" [dev-dependencies] starbase_sandbox = { workspace = true } - -[features] -default = [] -schematic = ["dep:schematic"] diff --git a/crates/core/src/events.rs b/crates/core/src/events.rs index 7c11cf168..ce93ff21e 100644 --- a/crates/core/src/events.rs +++ b/crates/core/src/events.rs @@ -1,5 +1,5 @@ -use crate::version::*; use starbase_events::Event; +use version_spec::*; macro_rules! impl_event { ($name:ident, $impl:tt) => { diff --git a/crates/core/src/helpers.rs b/crates/core/src/helpers.rs index f7c2dcb89..10d699841 100644 --- a/crates/core/src/helpers.rs +++ b/crates/core/src/helpers.rs @@ -14,7 +14,6 @@ use std::path::Path; use std::{env, path::PathBuf}; use tracing::trace; -pub static CLEAN_VERSION: Lazy = Lazy::new(|| Regex::new(r"([><]=?)\s+(\d)").unwrap()); pub static ENV_VAR: Lazy = Lazy::new(|| Regex::new(r"\$([A-Z0-9_]+)").unwrap()); #[deprecated = "Use `get_proto_home` instead."] @@ -54,40 +53,6 @@ pub fn get_plugins_dir() -> miette::Result { Ok(get_proto_home()?.join("plugins")) } -// Aliases are words that map to version. For example, "latest" -> "1.2.3". -pub fn is_alias_name>(value: T) -> bool { - let value = value.as_ref(); - - value.chars().enumerate().all(|(i, c)| { - if i == 0 { - char::is_ascii_alphabetic(&c) - } else { - char::is_ascii_alphanumeric(&c) - || c == '-' - || c == '_' - || c == '/' - || c == '.' - || c == '*' - } - }) -} - -pub fn remove_v_prefix>(value: T) -> String { - let value = value.as_ref(); - - if value.starts_with('v') || value.starts_with('V') { - return value[1..].to_owned(); - } - - value.to_owned() -} - -pub fn remove_space_after_gtlt>(value: T) -> String { - CLEAN_VERSION - .replace_all(value.as_ref(), "$1$2") - .to_string() -} - #[cached(time = 300)] #[tracing::instrument] pub fn is_offline() -> bool { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 6b0dcb71f..955fdcf42 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -8,7 +8,6 @@ mod tool_loader; mod tool_manifest; mod tools_config; mod user_config; -mod version; mod version_detector; mod version_resolver; @@ -24,7 +23,7 @@ pub use tool_loader::*; pub use tool_manifest::*; pub use tools_config::*; pub use user_config::*; -pub use version::*; pub use version_detector::*; pub use version_resolver::*; +pub use version_spec::*; pub use warpgate::*; diff --git a/crates/core/src/tool.rs b/crates/core/src/tool.rs index 9f64011ea..6f5f522bd 100644 --- a/crates/core/src/tool.rs +++ b/crates/core/src/tool.rs @@ -9,7 +9,6 @@ use crate::shimmer::{ create_global_shim, create_local_shim, get_shim_file_name, ShimContext, SHIM_VERSION, }; use crate::tool_manifest::ToolManifest; -use crate::version::{UnresolvedVersionSpec, VersionSpec}; use crate::version_resolver::VersionResolver; use extism::{manifest::Wasm, Manifest as PluginManifest}; use miette::IntoDiagnostic; @@ -27,6 +26,7 @@ use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; use tracing::{debug, trace}; +use version_spec::*; use warpgate::{download_from_url_to_file, Id, PluginContainer, VirtualPath}; pub struct Tool { @@ -330,7 +330,14 @@ impl Tool { if let Some(default) = sync_changes.default_version { modified = true; - self.manifest.default_version = Some(UnresolvedVersionSpec::parse(&default)?); + + self.manifest.default_version = + Some(UnresolvedVersionSpec::parse(&default).map_err(|error| { + ProtoError::Semver { + version: default, + error, + } + })?); } if let Some(versions) = sync_changes.versions { @@ -377,7 +384,13 @@ impl Tool { let mut versions = LoadVersionsOutput::default(); let mut cached = false; - let cache_path = self.get_inventory_dir().join("remote-versions.json"); + + // Don't use the overridden inventory path + let cache_path = self + .proto + .tools_dir + .join(self.id.as_str()) + .join("remote-versions.json"); // Attempt to read from the cache first if cache_path.exists() && (is_cache_enabled() || is_offline()) { @@ -448,7 +461,7 @@ impl Tool { if is_offline() && matches!(initial_version, UnresolvedVersionSpec::Version(_)) || matches!(initial_version, UnresolvedVersionSpec::Canary) { - let version = initial_version.to_spec(); + let version = initial_version.to_resolved_spec(); debug!( tool = self.id.as_str(), @@ -490,7 +503,12 @@ impl Tool { ); resolved = true; - version = resolver.resolve(&UnresolvedVersionSpec::parse(candidate)?)?; + version = resolver.resolve(&UnresolvedVersionSpec::parse(&candidate).map_err( + |error| ProtoError::Semver { + version: candidate, + error, + }, + )?)?; } if let Some(candidate) = result.version { @@ -501,7 +519,10 @@ impl Tool { ); resolved = true; - version = VersionSpec::parse(candidate)?; + version = VersionSpec::parse(&candidate).map_err(|error| ProtoError::Semver { + version: candidate, + error, + })?; } } @@ -583,7 +604,10 @@ impl Tool { "Detected a version" ); - return Ok(Some(UnresolvedVersionSpec::parse(version)?)); + return Ok(Some( + UnresolvedVersionSpec::parse(&version) + .map_err(|error| ProtoError::Semver { version, error })?, + )); } Ok(None) @@ -1164,7 +1188,9 @@ impl Tool { self.version .as_ref() // Canary can be overwritten so treat as not-installed - .is_some_and(|v| !v.is_latest() && !v.is_canary()) + .is_some_and(|v| { + !v.is_latest() && !v.is_canary() && self.manifest.installed_versions.contains(v) + }) && dir.exists() && !fs::is_dir_locked(dir) } @@ -1218,7 +1244,14 @@ impl Tool { let mut default = None; if let Some(default_version) = &self.metadata.default_version { - default = Some(UnresolvedVersionSpec::parse(default_version)?); + default = Some( + UnresolvedVersionSpec::parse(default_version).map_err(|error| { + ProtoError::Semver { + version: default_version.to_owned(), + error, + } + })?, + ); } self.manifest diff --git a/crates/core/src/tool_manifest.rs b/crates/core/src/tool_manifest.rs index c576dad5a..558ef18b1 100644 --- a/crates/core/src/tool_manifest.rs +++ b/crates/core/src/tool_manifest.rs @@ -1,7 +1,4 @@ -use crate::{ - helpers::{read_json_file_with_lock, write_json_file_with_lock}, - version::{UnresolvedVersionSpec, VersionSpec}, -}; +use crate::helpers::{read_json_file_with_lock, write_json_file_with_lock}; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashSet}, @@ -10,6 +7,7 @@ use std::{ time::SystemTime, }; use tracing::{debug, info}; +use version_spec::*; fn now() -> u128 { SystemTime::now() diff --git a/crates/core/src/tools_config.rs b/crates/core/src/tools_config.rs index af8db8ed3..db3777487 100644 --- a/crates/core/src/tools_config.rs +++ b/crates/core/src/tools_config.rs @@ -1,4 +1,3 @@ -use crate::version::UnresolvedVersionSpec; use miette::IntoDiagnostic; use serde::{Deserialize, Serialize}; use starbase_utils::{fs, toml}; @@ -6,6 +5,7 @@ use std::collections::BTreeMap; use std::env; use std::path::{Path, PathBuf}; use tracing::{debug, trace}; +use version_spec::*; use warpgate::{Id, PluginLocator}; pub const TOOLS_CONFIG_NAME: &str = ".prototools"; diff --git a/crates/core/src/version.rs b/crates/core/src/version.rs deleted file mode 100644 index 592c8eda1..000000000 --- a/crates/core/src/version.rs +++ /dev/null @@ -1,273 +0,0 @@ -#![allow(clippy::from_over_into)] - -use crate::error::ProtoError; -use crate::helpers::{is_alias_name, remove_space_after_gtlt, remove_v_prefix}; -use human_sort::compare; -use semver::{Version, VersionReq}; -use serde::{Deserialize, Serialize}; -use std::fmt::{Debug, Display}; -use std::str::FromStr; - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(untagged, into = "String", try_from = "String")] -pub enum UnresolvedVersionSpec { - Canary, - Alias(String), - Req(VersionReq), - ReqAny(Vec), - Version(Version), -} - -impl UnresolvedVersionSpec { - pub fn parse>(value: T) -> miette::Result { - Ok(Self::from_str(value.as_ref())?) - } - - pub fn is_canary(&self) -> bool { - matches!(self, UnresolvedVersionSpec::Canary) - } - - pub fn to_spec(&self) -> VersionSpec { - match self { - UnresolvedVersionSpec::Canary => VersionSpec::Alias("canary".to_owned()), - UnresolvedVersionSpec::Alias(alias) => VersionSpec::Alias(alias.to_owned()), - UnresolvedVersionSpec::Version(version) => VersionSpec::Version(version.to_owned()), - _ => unreachable!(), - } - } -} - -impl Default for UnresolvedVersionSpec { - fn default() -> Self { - Self::Alias("latest".into()) - } -} - -impl FromStr for UnresolvedVersionSpec { - type Err = ProtoError; - - fn from_str(value: &str) -> Result { - let value = remove_space_after_gtlt(remove_v_prefix(value.trim().replace(".*", ""))); - - if value == "canary" { - return Ok(UnresolvedVersionSpec::Canary); - } - - if is_alias_name(&value) { - return Ok(UnresolvedVersionSpec::Alias(value)); - } - - let handle_error = |error: semver::Error| ProtoError::Semver { - version: value.to_owned(), - error, - }; - - // OR requirements - if value.contains("||") { - let mut any = vec![]; - let mut parts = value.split("||").map(|p| p.trim()).collect::>(); - - // Try and sort from highest to lowest range - parts.sort_by(|a, d| compare(d, a)); - - for req in parts { - any.push(VersionReq::parse(req).map_err(handle_error)?); - } - - return Ok(UnresolvedVersionSpec::ReqAny(any)); - } - - // AND requirements - if value.contains(',') { - return Ok(UnresolvedVersionSpec::Req( - VersionReq::parse(&value).map_err(handle_error)?, - )); - } else if value.contains(' ') { - return Ok(UnresolvedVersionSpec::Req( - VersionReq::parse(&value.replace(' ', ", ")).map_err(handle_error)?, - )); - } - - Ok(match value.chars().next().unwrap() { - '=' | '^' | '~' | '>' | '<' | '*' => { - UnresolvedVersionSpec::Req(VersionReq::parse(&value).map_err(handle_error)?) - } - _ => { - let dot_count = value.match_indices('.').collect::>().len(); - - // If not fully qualified, match using a requirement - if dot_count < 2 { - UnresolvedVersionSpec::Req( - VersionReq::parse(&format!("~{value}")).map_err(handle_error)?, - ) - } else { - UnresolvedVersionSpec::Version(Version::parse(&value).map_err(handle_error)?) - } - } - }) - } -} - -impl TryFrom for UnresolvedVersionSpec { - type Error = ProtoError; - - fn try_from(value: String) -> Result { - Self::from_str(&value) - } -} - -impl Into for UnresolvedVersionSpec { - fn into(self) -> String { - self.to_string() - } -} - -impl Display for UnresolvedVersionSpec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Canary => write!(f, "canary"), - Self::Alias(alias) => write!(f, "{}", alias), - Self::Req(req) => write!(f, "{}", req), - Self::ReqAny(reqs) => write!( - f, - "{}", - reqs.iter() - .map(|req| req.to_string()) - .collect::>() - .join(" || ") - ), - Self::Version(version) => write!(f, "{}", version), - } - } -} - -impl PartialEq for UnresolvedVersionSpec { - fn eq(&self, other: &VersionSpec) -> bool { - match (self, other) { - (Self::Canary, VersionSpec::Alias(a)) => a == "canary", - (Self::Alias(a1), VersionSpec::Alias(a2)) => a1 == a2, - (Self::Version(v1), VersionSpec::Version(v2)) => v1 == v2, - _ => false, - } - } -} - -#[cfg(feature = "schematic")] -impl schematic::Schematic for UnresolvedVersionSpec { - fn generate_schema() -> schematic::SchemaType { - schematic::SchemaType::string() - } -} - -#[derive(Clone, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(untagged, into = "String", try_from = "String")] -pub enum VersionSpec { - Alias(String), - Version(Version), -} - -impl VersionSpec { - pub fn parse>(value: T) -> miette::Result { - Ok(Self::from_str(value.as_ref())?) - } - - pub fn is_canary(&self) -> bool { - match self { - Self::Alias(alias) => alias == "canary", - Self::Version(_) => false, - } - } - - pub fn is_latest(&self) -> bool { - match self { - Self::Alias(alias) => alias == "latest", - Self::Version(_) => false, - } - } - - pub fn to_unresolved_spec(&self) -> UnresolvedVersionSpec { - match self { - Self::Alias(alias) => UnresolvedVersionSpec::Alias(alias.to_owned()), - Self::Version(version) => UnresolvedVersionSpec::Version(version.to_owned()), - } - } -} - -impl Default for VersionSpec { - fn default() -> Self { - Self::Alias("latest".into()) - } -} - -impl FromStr for VersionSpec { - type Err = ProtoError; - - fn from_str(value: &str) -> Result { - let value = remove_space_after_gtlt(remove_v_prefix(value.trim().replace(".*", ""))); - - if is_alias_name(&value) { - return Ok(VersionSpec::Alias(value)); - } - - Ok(VersionSpec::Version(Version::parse(&value).map_err( - |error| ProtoError::Semver { - version: value, - error, - }, - )?)) - } -} - -impl TryFrom for VersionSpec { - type Error = ProtoError; - - fn try_from(value: String) -> Result { - Self::from_str(&value) - } -} - -impl Into for VersionSpec { - fn into(self) -> String { - self.to_string() - } -} - -impl Debug for VersionSpec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) - } -} - -impl Display for VersionSpec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Alias(alias) => write!(f, "{}", alias), - Self::Version(version) => write!(f, "{}", version), - } - } -} - -impl PartialEq<&str> for VersionSpec { - fn eq(&self, other: &&str) -> bool { - match self { - Self::Alias(alias) => alias == other, - Self::Version(version) => version.to_string() == *other, - } - } -} - -impl PartialEq for VersionSpec { - fn eq(&self, other: &Version) -> bool { - match self { - Self::Version(version) => version == other, - _ => false, - } - } -} - -#[cfg(feature = "schematic")] -impl schematic::Schematic for VersionSpec { - fn generate_schema() -> schematic::SchemaType { - schematic::SchemaType::string() - } -} diff --git a/crates/core/src/version_detector.rs b/crates/core/src/version_detector.rs index d105729d5..81e697934 100644 --- a/crates/core/src/version_detector.rs +++ b/crates/core/src/version_detector.rs @@ -1,9 +1,9 @@ use crate::error::ProtoError; use crate::tool::Tool; use crate::tools_config::ToolsConfig; -use crate::version::UnresolvedVersionSpec; use std::{env, path::Path}; use tracing::{debug, trace}; +use version_spec::*; pub async fn detect_version( tool: &Tool, @@ -23,7 +23,14 @@ pub async fn detect_version( "Detected version from environment variable", ); - candidate = Some(UnresolvedVersionSpec::parse(session_version)?); + candidate = Some( + UnresolvedVersionSpec::parse(&session_version).map_err(|error| { + ProtoError::Semver { + version: session_version, + error, + } + })?, + ); } else { trace!( tool = tool.id.as_str(), diff --git a/crates/core/src/version_resolver.rs b/crates/core/src/version_resolver.rs index c283d3fbd..1d6a367dc 100644 --- a/crates/core/src/version_resolver.rs +++ b/crates/core/src/version_resolver.rs @@ -1,10 +1,10 @@ use crate::error::ProtoError; use crate::tool_manifest::ToolManifest; -use crate::version::UnresolvedVersionSpec; use crate::VersionSpec; use proto_pdk_api::LoadVersionsOutput; use semver::{Version, VersionReq}; use std::collections::{BTreeMap, HashSet}; +use version_spec::*; #[derive(Debug, Default)] pub struct VersionResolver<'tool> { diff --git a/crates/version-spec/Cargo.toml b/crates/version-spec/Cargo.toml new file mode 100644 index 000000000..c3982b88e --- /dev/null +++ b/crates/version-spec/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "version_spec" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "A specification for working with partial, full, or aliased versions." +homepage = "https://moonrepo.dev/proto" +repository = "https://github.com/moonrepo/proto" + +[dependencies] +human-sort = { workspace = true } +once_cell = { workspace = true } +regex = { workspace = true } +schematic = { version = "0.11.7", default-features = false, optional = true } +semver = { workspace = true } +serde = { workspace = true } + +[features] +default = [] +schematic = ["dep:schematic"] diff --git a/crates/version-spec/README.md b/crates/version-spec/README.md new file mode 100644 index 000000000..7dddad9ee --- /dev/null +++ b/crates/version-spec/README.md @@ -0,0 +1,5 @@ +# version_spec + +![Crates.io](https://img.shields.io/crates/v/version_spec) ![Crates.io](https://img.shields.io/crates/d/version_spec) + +Enums and utilities for working with partial, full, and aliased semantic versions, known as a version specification. It primarily handles the states of an unresoled version candidate (requirement, range, alias, partial, etc) to a resolved version (version, alias). diff --git a/crates/version-spec/src/lib.rs b/crates/version-spec/src/lib.rs new file mode 100644 index 000000000..9a29f7885 --- /dev/null +++ b/crates/version-spec/src/lib.rs @@ -0,0 +1,41 @@ +mod resolved_spec; +mod unresolved_spec; + +use once_cell::sync::Lazy; +use regex::Regex; + +pub use resolved_spec::*; +pub use unresolved_spec::*; + +/// Aliases are words that map to version. For example, "latest" -> "1.2.3". +pub fn is_alias_name>(value: T) -> bool { + let value = value.as_ref(); + + value.chars().enumerate().all(|(i, c)| { + if i == 0 { + char::is_ascii_alphabetic(&c) + } else { + char::is_ascii_alphanumeric(&c) + || c == '-' + || c == '_' + || c == '/' + || c == '.' + || c == '*' + } + }) +} + +pub static CLEAN_VERSION: Lazy = Lazy::new(|| Regex::new(r"([><]=?)\s+(\d)").unwrap()); + +pub fn clean_version_string>(value: T) -> String { + let value = value.as_ref().trim().replace(".*", ""); + let mut version = value.as_str(); + + // Remove a leading "v" or "V" from a version string. + if version.starts_with('v') || version.starts_with('V') { + version = &version[1..]; + } + + // Remove invalid space after <, <=, >, >=. + CLEAN_VERSION.replace_all(version, "$1$2").to_string() +} diff --git a/crates/version-spec/src/resolved_spec.rs b/crates/version-spec/src/resolved_spec.rs new file mode 100644 index 000000000..e92b4ee5c --- /dev/null +++ b/crates/version-spec/src/resolved_spec.rs @@ -0,0 +1,116 @@ +#![allow(clippy::from_over_into)] + +use crate::{clean_version_string, is_alias_name, UnresolvedVersionSpec}; +use semver::{Error, Version}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Display}; +use std::str::FromStr; + +#[derive(Clone, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(untagged, into = "String", try_from = "String")] +pub enum VersionSpec { + Alias(String), + Version(Version), +} + +impl VersionSpec { + pub fn parse>(value: T) -> Result { + Self::from_str(value.as_ref()) + } + + pub fn is_canary(&self) -> bool { + match self { + Self::Alias(alias) => alias == "canary", + _ => false, + } + } + + pub fn is_latest(&self) -> bool { + match self { + Self::Alias(alias) => alias == "latest", + _ => false, + } + } + + pub fn to_unresolved_spec(&self) -> UnresolvedVersionSpec { + match self { + Self::Alias(alias) => UnresolvedVersionSpec::Alias(alias.to_owned()), + Self::Version(version) => UnresolvedVersionSpec::Version(version.to_owned()), + } + } +} + +impl Default for VersionSpec { + fn default() -> Self { + Self::Alias("latest".into()) + } +} + +impl FromStr for VersionSpec { + type Err = Error; + + fn from_str(value: &str) -> Result { + let value = clean_version_string(value); + + if is_alias_name(&value) { + return Ok(VersionSpec::Alias(value)); + } + + Ok(VersionSpec::Version(Version::parse(&value)?)) + } +} + +impl TryFrom for VersionSpec { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + +impl Into for VersionSpec { + fn into(self) -> String { + self.to_string() + } +} + +impl Debug for VersionSpec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Debug version as a string instead of a struct + write!(f, "{}", self) + } +} + +impl Display for VersionSpec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Alias(alias) => write!(f, "{}", alias), + Self::Version(version) => write!(f, "{}", version), + } + } +} + +impl PartialEq<&str> for VersionSpec { + fn eq(&self, other: &&str) -> bool { + match self { + Self::Alias(alias) => alias == other, + Self::Version(version) => version.to_string() == *other, + } + } +} + +impl PartialEq for VersionSpec { + fn eq(&self, other: &Version) -> bool { + match self { + Self::Version(version) => version == other, + _ => false, + } + } +} + +#[cfg(feature = "schematic")] +impl schematic::Schematic for VersionSpec { + fn generate_schema() -> schematic::SchemaType { + schematic::SchemaType::string() + } +} diff --git a/crates/version-spec/src/unresolved_spec.rs b/crates/version-spec/src/unresolved_spec.rs new file mode 100644 index 000000000..d0f7d3bdd --- /dev/null +++ b/crates/version-spec/src/unresolved_spec.rs @@ -0,0 +1,150 @@ +#![allow(clippy::from_over_into)] + +use crate::{clean_version_string, is_alias_name, VersionSpec}; +use human_sort::compare; +use semver::{Error, Version, VersionReq}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Display}; +use std::str::FromStr; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(untagged, into = "String", try_from = "String")] +pub enum UnresolvedVersionSpec { + Canary, + Alias(String), + Req(VersionReq), + ReqAny(Vec), + Version(Version), +} + +impl UnresolvedVersionSpec { + pub fn parse>(value: T) -> Result { + Self::from_str(value.as_ref()) + } + + pub fn is_canary(&self) -> bool { + matches!(self, UnresolvedVersionSpec::Canary) + } + + pub fn to_resolved_spec(&self) -> VersionSpec { + match self { + UnresolvedVersionSpec::Canary => VersionSpec::Alias("canary".to_owned()), + UnresolvedVersionSpec::Alias(alias) => VersionSpec::Alias(alias.to_owned()), + UnresolvedVersionSpec::Version(version) => VersionSpec::Version(version.to_owned()), + _ => unreachable!(), + } + } +} + +impl Default for UnresolvedVersionSpec { + fn default() -> Self { + Self::Alias("latest".into()) + } +} + +impl FromStr for UnresolvedVersionSpec { + type Err = Error; + + fn from_str(value: &str) -> Result { + let value = clean_version_string(value); + + if value == "canary" { + return Ok(UnresolvedVersionSpec::Canary); + } + + if is_alias_name(&value) { + return Ok(UnresolvedVersionSpec::Alias(value)); + } + + // OR requirements (Node.js) + if value.contains("||") { + let mut any = vec![]; + let mut parts = value.split("||").map(|p| p.trim()).collect::>(); + + // Try and sort from highest to lowest range + parts.sort_by(|a, d| compare(d, a)); + + for req in parts { + any.push(VersionReq::parse(req)?); + } + + return Ok(UnresolvedVersionSpec::ReqAny(any)); + } + + // AND requirements + if value.contains(',') { + return Ok(UnresolvedVersionSpec::Req(VersionReq::parse(&value)?)); + } else if value.contains(' ') { + return Ok(UnresolvedVersionSpec::Req(VersionReq::parse( + &value.replace(' ', ", "), + )?)); + } + + Ok(match value.chars().next().unwrap() { + '=' | '^' | '~' | '>' | '<' | '*' => { + UnresolvedVersionSpec::Req(VersionReq::parse(&value)?) + } + _ => { + let dot_count = value.match_indices('.').collect::>().len(); + + // If not fully qualified, match using a requirement + if dot_count < 2 { + UnresolvedVersionSpec::Req(VersionReq::parse(&format!("~{value}"))?) + } else { + UnresolvedVersionSpec::Version(Version::parse(&value)?) + } + } + }) + } +} + +impl TryFrom for UnresolvedVersionSpec { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + +impl Into for UnresolvedVersionSpec { + fn into(self) -> String { + self.to_string() + } +} + +impl Display for UnresolvedVersionSpec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Canary => write!(f, "canary"), + Self::Alias(alias) => write!(f, "{}", alias), + Self::Req(req) => write!(f, "{}", req), + Self::ReqAny(reqs) => write!( + f, + "{}", + reqs.iter() + .map(|req| req.to_string()) + .collect::>() + .join(" || ") + ), + Self::Version(version) => write!(f, "{}", version), + } + } +} + +impl PartialEq for UnresolvedVersionSpec { + fn eq(&self, other: &VersionSpec) -> bool { + match (self, other) { + (Self::Canary, VersionSpec::Alias(a)) => a == "canary", + (Self::Alias(a1), VersionSpec::Alias(a2)) => a1 == a2, + (Self::Version(v1), VersionSpec::Version(v2)) => v1 == v2, + _ => false, + } + } +} + +#[cfg(feature = "schematic")] +impl schematic::Schematic for UnresolvedVersionSpec { + fn generate_schema() -> schematic::SchemaType { + schematic::SchemaType::string() + } +} diff --git a/crates/warpgate/Cargo.toml b/crates/warpgate/Cargo.toml index d0a2525b3..d640253e9 100644 --- a/crates/warpgate/Cargo.toml +++ b/crates/warpgate/Cargo.toml @@ -13,7 +13,7 @@ miette = { workspace = true } once_cell = { workspace = true } once_map = { workspace = true } regex = { workspace = true } -reqwest = { workspace = true, features = ["json"] } +reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots"] } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true }