diff --git a/Cargo.toml b/Cargo.toml index 52fbae39..ef16c650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ reqwest = { version = "0.11.22", default-features = false, features = [ revision = "0.5.0" rmpv = "1.0.1" rustyline = { version = "12.0.0", features = ["derive"] } +semver = "1.0.20" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" serde_pack = { version = "1.1.2", package = "rmp-serde" } diff --git a/src/cli/upgrade.rs b/src/cli/upgrade.rs index 9496bf6b..2f89e719 100644 --- a/src/cli/upgrade.rs +++ b/src/cli/upgrade.rs @@ -1,6 +1,7 @@ use crate::cnf::PKG_VERSION; use crate::err::Error; use clap::Args; +use semver::{Comparator, Op, Version}; use std::borrow::Cow; use std::fs; use std::io::{Error as IoError, ErrorKind}; @@ -30,18 +31,51 @@ pub struct UpgradeCommandArguments { impl UpgradeCommandArguments { /// Get the version string to download based on the user preference async fn version(&self) -> Result, Error> { - if self.nightly { - Ok(Cow::Borrowed("nightly")) - } else if self.beta { - fetch("beta").await - } else if let Some(version) = self.version.as_ref() { - Ok(Cow::Borrowed(version)) + let nightly = "nightly"; + let beta = "beta"; + // Convert the version to lowercase, if supplied + let version = self.version.as_deref().map(str::to_ascii_lowercase); + + if self.nightly || version.as_deref() == Some(nightly) { + Ok(Cow::Borrowed(nightly)) + } else if self.beta || version.as_deref() == Some(beta) { + fetch(beta).await + } else if let Some(version) = version { + // Parse the version string to make sure it's valid, return an error if not + let version = parse_version(&version)?; + // Return the version, ensuring it's prefixed by `v` + Ok(Cow::Owned(format!("v{version}"))) } else { fetch("latest").await } } } +fn parse_version(input: &str) -> Result { + // Remove the `v` prefix, if supplied + let version = input.strip_prefix('v').unwrap_or(input); + // Parse the version + let comp = Comparator::parse(version) + .map_err(|_| Error::Other(format!("Invalid version `{input}`",)))?; + // See if a supported operation was requested + if !matches!(comp.op, Op::Exact | Op::Caret) { + return Err(Error::Other(format!( + "Unsupported version `{version}`. Only exact matches are supported." + ))); + } + // Build and return the version if supported + match (comp.minor, comp.patch) { + (Some(minor), Some(patch)) => { + let mut version = Version::new(comp.major, minor, patch); + version.pre = comp.pre; + Ok(version) + } + _ => Err(Error::Other(format!( + "Unsupported version `{version}`. Please specify a full version, like `v1.2.1`." + ))), + } +} + async fn fetch(version: &str) -> Result, Error> { let response = reqwest::get(format!("{ROOT}/{version}.txt")).await?; if !response.status().is_success() {