summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2025-02-05 13:08:20 +0100
committerLinus Färnstrand <linus@mullvad.net>2025-02-18 19:12:22 +0100
commit35fb1310a4dd348d33e1ed2454b47bb2c2819077 (patch)
treee740d6585e2d5591a9aa2d3106c69845783454c9
parent447ec20b79adbda18d6e954a3d30178ffeb35a67 (diff)
downloadmullvadvpn-35fb1310a4dd348d33e1ed2454b47bb2c2819077.tar.xz
mullvadvpn-35fb1310a4dd348d33e1ed2454b47bb2c2819077.zip
Unify daemon app version types
Previously we had two types in the code base that dealt with version parsing. This commit unifies these types so that we only use the Version struct that is defines in the mullvad-version crate. This also solves a bug where the daemon code would crash on alpha versions, as the previous version parsing code didn't handle them.
-rw-r--r--mullvad-daemon/src/version_check.rs72
-rw-r--r--mullvad-setup/src/main.rs13
-rw-r--r--mullvad-types/src/version.rs176
-rw-r--r--mullvad-version/src/lib.rs312
-rw-r--r--mullvad-version/src/main.rs39
-rw-r--r--test/test-manager/src/tests/install.rs6
6 files changed, 303 insertions, 315 deletions
diff --git a/mullvad-daemon/src/version_check.rs b/mullvad-daemon/src/version_check.rs
index 1ee879ca1f..c5da930c27 100644
--- a/mullvad-daemon/src/version_check.rs
+++ b/mullvad-daemon/src/version_check.rs
@@ -5,10 +5,10 @@ use futures::{
FutureExt, SinkExt, StreamExt, TryFutureExt,
};
use mullvad_api::{availability::ApiAvailability, rest::MullvadRestHandle, AppVersionProxy};
-use mullvad_types::version::{AppVersionInfo, ParsedAppVersion};
+use mullvad_types::version::AppVersionInfo;
+use mullvad_version::Version;
use serde::{Deserialize, Serialize};
use std::{
- cmp::max,
future::Future,
io,
path::{Path, PathBuf},
@@ -24,8 +24,8 @@ use tokio::{fs::File, io::AsyncReadExt};
const VERSION_INFO_FILENAME: &str = "version-info.json";
-static APP_VERSION: LazyLock<ParsedAppVersion> =
- LazyLock::new(|| ParsedAppVersion::from_str(mullvad_version::VERSION).unwrap());
+static APP_VERSION: LazyLock<Version> =
+ LazyLock::new(|| Version::from_str(mullvad_version::VERSION).unwrap());
static IS_DEV_BUILD: LazyLock<bool> = LazyLock::new(|| APP_VERSION.is_dev());
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(15);
@@ -535,26 +535,38 @@ fn dev_version_cache() -> AppVersionInfo {
suggested_upgrade: None,
}
}
+
/// If current_version is not the latest, return a string containing the latest version.
fn suggested_upgrade(
- current_version: &ParsedAppVersion,
+ current_version: &Version,
latest_stable: &Option<String>,
latest_beta: &str,
show_beta: bool,
) -> Option<String> {
let stable_version = latest_stable
.as_ref()
- .and_then(|stable| ParsedAppVersion::from_str(stable).ok());
+ .and_then(|stable| Version::from_str(stable).ok());
let beta_version = if show_beta {
- ParsedAppVersion::from_str(latest_beta).ok()
+ Version::from_str(latest_beta).ok()
} else {
None
};
- let latest_version = max(stable_version, beta_version)?;
+ let latest_version = match (&stable_version, &beta_version) {
+ (Some(_), None) => stable_version,
+ (None, Some(_)) => beta_version,
+ (Some(stable), Some(beta)) => {
+ if beta > stable {
+ beta_version
+ } else {
+ stable_version
+ }
+ }
+ (None, None) => None,
+ }?;
- if current_version < &latest_version {
+ if &latest_version > current_version {
Some(latest_version.to_string())
} else {
None
@@ -736,13 +748,17 @@ mod test {
let latest_stable = Some("2020.4".to_string());
let latest_beta = "2020.5-beta3";
- let older_stable = ParsedAppVersion::from_str("2020.3").unwrap();
- let current_stable = ParsedAppVersion::from_str("2020.4").unwrap();
- let newer_stable = ParsedAppVersion::from_str("2021.5").unwrap();
+ let older_stable = Version::from_str("2020.3").unwrap();
+ let current_stable = Version::from_str("2020.4").unwrap();
+ let newer_stable = Version::from_str("2021.5").unwrap();
- let older_beta = ParsedAppVersion::from_str("2020.3-beta3").unwrap();
- let current_beta = ParsedAppVersion::from_str("2020.5-beta3").unwrap();
- let newer_beta = ParsedAppVersion::from_str("2021.5-beta3").unwrap();
+ let older_beta = Version::from_str("2020.3-beta3").unwrap();
+ let current_beta = Version::from_str("2020.5-beta3").unwrap();
+ let newer_beta = Version::from_str("2021.5-beta3").unwrap();
+
+ let older_alpha = Version::from_str("2020.3-alpha3").unwrap();
+ let current_alpha = Version::from_str("2020.5-alpha3").unwrap();
+ let newer_alpha = Version::from_str("2021.5-alpha3").unwrap();
assert_eq!(
suggested_upgrade(&older_stable, &latest_stable, latest_beta, false),
@@ -768,6 +784,7 @@ mod test {
suggested_upgrade(&newer_stable, &latest_stable, latest_beta, true),
None
);
+
assert_eq!(
suggested_upgrade(&older_beta, &latest_stable, latest_beta, false),
Some("2020.4".to_owned())
@@ -792,5 +809,30 @@ mod test {
suggested_upgrade(&newer_beta, &latest_stable, latest_beta, true),
None
);
+
+ assert_eq!(
+ suggested_upgrade(&older_alpha, &latest_stable, latest_beta, false),
+ Some("2020.4".to_owned())
+ );
+ assert_eq!(
+ suggested_upgrade(&older_alpha, &latest_stable, latest_beta, true),
+ Some("2020.5-beta3".to_owned())
+ );
+ assert_eq!(
+ suggested_upgrade(&current_alpha, &latest_stable, latest_beta, false),
+ None,
+ );
+ assert_eq!(
+ suggested_upgrade(&current_alpha, &latest_stable, latest_beta, true),
+ Some("2020.5-beta3".to_owned())
+ );
+ assert_eq!(
+ suggested_upgrade(&newer_alpha, &latest_stable, latest_beta, false),
+ None
+ );
+ assert_eq!(
+ suggested_upgrade(&newer_alpha, &latest_stable, latest_beta, true),
+ None
+ );
}
}
diff --git a/mullvad-setup/src/main.rs b/mullvad-setup/src/main.rs
index e525d5cb88..166364cf92 100644
--- a/mullvad-setup/src/main.rs
+++ b/mullvad-setup/src/main.rs
@@ -1,15 +1,14 @@
use clap::Parser;
-use std::{path::PathBuf, process, str::FromStr, sync::LazyLock, time::Duration};
-
use mullvad_api::{proxy::ApiConnectionMode, ApiEndpoint, DEVICE_NOT_FOUND};
use mullvad_management_interface::MullvadProxyClient;
-use mullvad_types::version::ParsedAppVersion;
+use mullvad_version::Version;
+use std::{path::PathBuf, process, str::FromStr, sync::LazyLock, time::Duration};
use talpid_core::firewall::{self, Firewall};
use talpid_future::retry::{retry_future, ConstantInterval};
use talpid_types::ErrorExt;
-static APP_VERSION: LazyLock<ParsedAppVersion> =
- LazyLock::new(|| ParsedAppVersion::from_str(mullvad_version::VERSION).unwrap());
+static APP_VERSION: LazyLock<Version> =
+ LazyLock::new(|| Version::from_str(mullvad_version::VERSION).unwrap());
const DEVICE_REMOVAL_STRATEGY: ConstantInterval = ConstantInterval::new(Duration::ZERO, Some(5));
@@ -114,9 +113,9 @@ async fn main() {
fn is_older_version(old_version: &str) -> Result<ExitStatus, Error> {
let parsed_version =
- ParsedAppVersion::from_str(old_version).map_err(|_| Error::ParseVersionStringError)?;
+ Version::from_str(old_version).map_err(|_| Error::ParseVersionStringError)?;
- Ok(if parsed_version < *APP_VERSION {
+ Ok(if *APP_VERSION > parsed_version {
ExitStatus::Ok
} else {
ExitStatus::VersionNotOlder
diff --git a/mullvad-types/src/version.rs b/mullvad-types/src/version.rs
index 1ca5785147..6338909feb 100644
--- a/mullvad-types/src/version.rs
+++ b/mullvad-types/src/version.rs
@@ -1,17 +1,4 @@
-use regex::Regex;
use serde::{Deserialize, Serialize};
-use std::{
- cmp::Ordering,
- fmt::{self, Formatter},
- str::FromStr,
- sync::LazyLock,
-};
-
-static STABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\d{4})\.(\d+)$").unwrap());
-static BETA_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"^(\d{4})\.(\d+)-beta(\d+)$").unwrap());
-static DEV_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"^(\d{4})\.(\d+)(\.\d+)?(-beta(\d+))?-dev-(\w+)$").unwrap());
/// AppVersionInfo represents the current stable and the current latest release versions of the
/// Mullvad VPN app.
@@ -35,166 +22,3 @@ pub struct AppVersionInfo {
}
pub type AppVersion = String;
-
-/// Parses a version string into a type that can be used for comparisons.
-#[derive(Eq, PartialEq, Debug, Clone)]
-pub enum ParsedAppVersion {
- Stable(u32, u32),
- Beta(u32, u32, u32),
- Dev(u32, u32, Option<u32>, String),
-}
-
-impl FromStr for ParsedAppVersion {
- type Err = ();
- fn from_str(version: &str) -> Result<Self, Self::Err> {
- let get_int = |cap: &regex::Captures<'_>, idx| cap.get(idx)?.as_str().parse().ok();
-
- if let Some(caps) = STABLE_REGEX.captures(version) {
- let year = get_int(&caps, 1).ok_or(())?;
- let version = get_int(&caps, 2).ok_or(())?;
- Ok(Self::Stable(year, version))
- } else if let Some(caps) = BETA_REGEX.captures(version) {
- let year = get_int(&caps, 1).ok_or(())?;
- let version = get_int(&caps, 2).ok_or(())?;
- let beta_version = get_int(&caps, 3).ok_or(())?;
- Ok(Self::Beta(year, version, beta_version))
- } else if let Some(caps) = DEV_REGEX.captures(version) {
- let year = get_int(&caps, 1).ok_or(())?;
- let version = get_int(&caps, 2).ok_or(())?;
- let beta_version = caps.get(4).map(|_| get_int(&caps, 5).unwrap());
- let dev_hash = caps.get(6).ok_or(())?.as_str().to_string();
- Ok(Self::Dev(year, version, beta_version, dev_hash))
- } else {
- Err(())
- }
- }
-}
-
-impl ParsedAppVersion {
- pub fn is_dev(&self) -> bool {
- matches!(self, ParsedAppVersion::Dev(..))
- }
-}
-
-impl Ord for ParsedAppVersion {
- fn cmp(&self, other: &Self) -> Ordering {
- use ParsedAppVersion::*;
- match (self, other) {
- (Stable(year, version), Stable(other_year, other_version)) => {
- year.cmp(other_year).then(version.cmp(other_version))
- }
- // A stable version of the same year and version is always greater than a beta
- (Stable(year, version), Beta(other_year, other_version, _)) => year
- .cmp(other_year)
- .then(version.cmp(other_version))
- .then(Ordering::Greater),
- // We assume that a dev version of the same year and version is newer
- (Stable(year, version), Dev(other_year, other_version, ..)) => year
- .cmp(other_year)
- .then(version.cmp(other_version))
- .then(Ordering::Less),
-
- (
- Beta(year, version, beta_version),
- Beta(other_year, other_version, other_beta_version),
- ) => year
- .cmp(other_year)
- .then(version.cmp(other_version))
- .then(beta_version.cmp(other_beta_version)),
- (Beta(year, version, _beta_version), Stable(other_year, other_version)) => year
- .cmp(other_year)
- .then(version.cmp(other_version))
- .then(Ordering::Less),
- // We assume that a dev version of the same year and version is newer
- (Beta(year, version, _), Dev(other_year, other_version, ..)) => year
- .cmp(other_year)
- .then(version.cmp(other_version))
- .then(Ordering::Less),
-
- // Dev versions of the same year and version are assumed to be equal
- (Dev(year, version, ..), Dev(other_year, other_version, ..)) => {
- year.cmp(other_year).then(version.cmp(other_version))
- }
- (Dev(year, version, ..), Stable(other_year, other_version)) => year
- .cmp(other_year)
- .then(version.cmp(other_version))
- .then(Ordering::Greater),
- (Dev(year, version, ..), Beta(other_year, other_version, _)) => year
- .cmp(other_year)
- .then(version.cmp(other_version))
- .then(Ordering::Greater),
- }
- }
-}
-
-impl PartialOrd for ParsedAppVersion {
- fn partial_cmp(&self, other: &ParsedAppVersion) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl fmt::Display for ParsedAppVersion {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- match self {
- Self::Stable(year, version) => write!(f, "{year}.{version}"),
- Self::Beta(year, version, beta_version) => {
- write!(f, "{year}.{version}-beta{beta_version}")
- }
- Self::Dev(year, version, beta_version, hash) => {
- if let Some(beta_version) = beta_version {
- write!(f, "{year}.{version}-beta{beta_version}-dev-{hash}")
- } else {
- write!(f, "{year}.{version}-dev-{hash}")
- }
- }
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_version_regex() {
- assert!(STABLE_REGEX.is_match("2020.4"));
- assert!(!STABLE_REGEX.is_match("2020.4-beta3"));
- assert!(BETA_REGEX.is_match("2020.4-beta3"));
- assert!(!STABLE_REGEX.is_match("2020.5-beta1-dev-f16be4"));
- assert!(!STABLE_REGEX.is_match("2020.5-dev-f16be4"));
- assert!(!BETA_REGEX.is_match("2020.5-beta1-dev-f16be4"));
- assert!(!BETA_REGEX.is_match("2020.5-dev-f16be4"));
- assert!(!BETA_REGEX.is_match("2020.4"));
- assert!(DEV_REGEX.is_match("2020.5-dev-f16be4"));
- assert!(DEV_REGEX.is_match("2020.5-beta1-dev-f16be4"));
- assert!(!DEV_REGEX.is_match("2020.5"));
- assert!(!DEV_REGEX.is_match("2020.5-beta1"));
- }
-
- #[test]
- fn test_version_parsing() {
- let tests = vec![
- ("2020.4", Some(ParsedAppVersion::Stable(2020, 4))),
- ("2020.4-beta3", Some(ParsedAppVersion::Beta(2020, 4, 3))),
- (
- "2020.15-beta1-dev-f16be4",
- Some(ParsedAppVersion::Dev(
- 2020,
- 15,
- Some(1),
- "f16be4".to_string(),
- )),
- ),
- (
- "2020.15-dev-f16be4",
- Some(ParsedAppVersion::Dev(2020, 15, None, "f16be4".to_string())),
- ),
- ("2020.15-9000", None),
- ("", None),
- ];
-
- for (input, expected_output) in tests {
- assert_eq!(ParsedAppVersion::from_str(input).ok(), expected_output,);
- }
- }
-}
diff --git a/mullvad-version/src/lib.rs b/mullvad-version/src/lib.rs
index cb9ecde750..29ecb78a1e 100644
--- a/mullvad-version/src/lib.rs
+++ b/mullvad-version/src/lib.rs
@@ -1,8 +1,8 @@
+use std::cmp::Ordering;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
-use crate::PreStableType::{Alpha, Beta};
use regex::Regex;
/// The Mullvad VPN app product version
@@ -10,42 +10,72 @@ pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/product-versio
#[derive(Debug, Clone, PartialEq)]
pub struct Version {
- /// The last two digits of the version's year
- pub year: String,
- pub incremental: String,
- /// A version can have an optional pre-stable type, e.g. alpha or beta. If `pre_stable`
- /// and `dev` both are None the version is stable.
+ pub year: u32,
+ pub incremental: u32,
+ /// A version can have an optional pre-stable type, e.g. alpha or beta.
pub pre_stable: Option<PreStableType>,
/// All versions may have an optional -dev-[commit hash] suffix.
pub dev: Option<String>,
}
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, Eq, PartialEq)]
pub enum PreStableType {
- Alpha(String),
- Beta(String),
+ Alpha(u32),
+ Beta(u32),
}
-impl Version {
- pub fn parse(version: &str) -> Version {
- Version::from_str(version).unwrap()
+impl Ord for PreStableType {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match (self, other) {
+ (PreStableType::Alpha(a), PreStableType::Alpha(b)) => a.cmp(b),
+ (PreStableType::Beta(a), PreStableType::Beta(b)) => a.cmp(b),
+ (PreStableType::Alpha(_), PreStableType::Beta(_)) => Ordering::Less,
+ (PreStableType::Beta(_), PreStableType::Alpha(_)) => Ordering::Greater,
+ }
}
+}
- pub fn is_stable(&self) -> bool {
- self.pre_stable.is_none() && self.dev.is_none()
+impl PartialOrd for PreStableType {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
}
+}
- pub fn alpha(&self) -> Option<&str> {
- match &self.pre_stable {
- Some(PreStableType::Alpha(v)) => Some(v),
- _ => None,
- }
+impl Version {
+ /// Returns true if this version has a -dev suffix, e.g. 2025.2-beta1-dev-123abc
+ pub fn is_dev(&self) -> bool {
+ self.dev.is_some()
}
+}
- pub fn beta(&self) -> Option<&str> {
- match &self.pre_stable {
- Some(PreStableType::Beta(beta)) => Some(beta),
- _ => None,
+impl PartialOrd for Version {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ let type_ordering = match (&self.pre_stable, &other.pre_stable) {
+ (None, None) => Ordering::Equal,
+ (Some(_), None) => Ordering::Less,
+ (None, Some(_)) => Ordering::Greater,
+ (Some(self_pre_stable), Some(other_pre_stable)) => {
+ self_pre_stable.cmp(other_pre_stable)
+ }
+ };
+
+ // The dev vs non-dev ordering. For a version of a given type, if all else is equal
+ // a dev version is greater than a non-dev version.
+ let dev_ordering = match (self.is_dev(), other.is_dev()) {
+ (true, false) => Some(Ordering::Greater),
+ (false, true) => Some(Ordering::Less),
+ (_, _) => None,
+ };
+
+ let release_ordering = self
+ .year
+ .cmp(&other.year)
+ .then(self.incremental.cmp(&other.incremental))
+ .then(type_ordering);
+
+ match release_ordering {
+ Ordering::Equal => dev_ordering,
+ _ => Some(release_ordering),
}
}
}
@@ -60,7 +90,7 @@ impl Display for Version {
dev,
} = &self;
- write!(f, "20{year}.{incremental}")?;
+ write!(f, "{year}.{incremental}")?;
match pre_stable {
Some(PreStableType::Alpha(version)) => write!(f, "-alpha{version}")?,
@@ -76,14 +106,10 @@ impl Display for Version {
}
}
-impl FromStr for Version {
- type Err = String;
-
- fn from_str(version: &str) -> Result<Self, Self::Err> {
- static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
- Regex::new(
- r"(?x) # enable insignificant whitespace mode
- 20(?<year>\d{2})\. # the last two digits of the year
+static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
+ Regex::new(
+ r"(?x) # enable insignificant whitespace mode
+ (?<year>\d{4})\. # the year
(?<incremental>[1-9]\d?) # the incrementing version number
(?: # (optional) alpha or beta or dev
-alpha(?<alpha>[1-9]\d?\d?)|
@@ -93,34 +119,35 @@ impl FromStr for Version {
-dev-(?<dev>[0-9a-f]+)
)?$
",
- )
- .unwrap()
- });
+ )
+ .unwrap()
+});
+impl FromStr for Version {
+ type Err = String;
+
+ fn from_str(version: &str) -> Result<Self, Self::Err> {
let captures = VERSION_REGEX
.captures(version)
.ok_or_else(|| format!("Version does not match expected format: {version}"))?;
- let year = captures
- .name("year")
- .expect("Missing year")
- .as_str()
- .to_owned();
+ let year = captures.name("year").unwrap().as_str().parse().unwrap();
let incremental = captures
.name("incremental")
- .ok_or("Missing incremental")?
+ .unwrap()
.as_str()
- .to_owned();
+ .parse()
+ .unwrap();
- let alpha = captures.name("alpha").map(|m| m.as_str().to_owned());
- let beta = captures.name("beta").map(|m| m.as_str().to_owned());
+ let alpha = captures.name("alpha").map(|m| m.as_str().parse().unwrap());
+ let beta = captures.name("beta").map(|m| m.as_str().parse().unwrap());
let dev = captures.name("dev").map(|m| m.as_str().to_owned());
let pre_stable = match (alpha, beta) {
(None, None) => None,
- (Some(v), None) => Some(Alpha(v)),
- (None, Some(v)) => Some(Beta(v)),
+ (Some(v), None) => Some(PreStableType::Alpha(v)),
+ (None, Some(v)) => Some(PreStableType::Beta(v)),
_ => return Err(format!("Invalid version: {version}")),
};
@@ -138,102 +165,201 @@ mod tests {
use super::*;
#[test]
+ fn test_version_ordering() {
+ // Test year comparison
+ assert!(parse("2022.1") > parse("2021.1"),);
+
+ // Test incremental comparison
+ assert!(parse("2021.2") > parse("2021.1"),);
+
+ // Test stable vs pre-release
+ assert!(parse("2021.1") > parse("2021.1-beta1"),);
+ assert!(parse("2021.1") > parse("2021.1-alpha1"),);
+
+ // Test beta vs alpha
+ assert!(parse("2021.1-beta1") > parse("2021.1-alpha1"),);
+ assert!(parse("2021.1-beta1") > parse("2021.1-alpha2"),);
+ assert!(parse("2021.2-alpha1") > parse("2021.1-beta2"),);
+
+ // Test version numbers within same type
+ assert!(parse("2021.1-beta2") > parse("2021.1-beta1"),);
+ assert!(parse("2021.1-alpha2") > parse("2021.1-alpha1"),);
+
+ // Test dev versions
+ assert!(parse("2021.1-dev-abc") > parse("2021.1"),);
+ assert!(parse("2021.2") > parse("2021.1-dev-abc"),);
+ assert!(parse("2021.1-dev-abc") > parse("2021.1-beta1"),);
+ assert!(parse("2021.1-dev-abc") > parse("2021.1-alpha1"),);
+ assert!(parse("2025.1-dev-abc") > parse("2025.1-beta1-dev-abc"),);
+ assert!(parse("2025.1-dev-abc") > parse("2025.1-beta2-dev-abc"),);
+ assert!(parse("2025.1-dev-abc") > parse("2025.1-alpha2-dev-abc"),);
+ assert!(parse("2025.1-beta1-dev-abc") > parse("2025.1-alpha7-dev-abc"),);
+ assert!(parse("2025.2-alpha1-dev-abc") > parse("2025.1-beta7-dev-abc"),);
+
+ // Test version equality
+ assert_eq!(parse("2021.1"), parse("2021.1"));
+ assert_eq!(parse("2021.1-beta1"), parse("2021.1-beta1"));
+ assert_eq!(parse("2021.1-alpha7"), parse("2021.1-alpha7"));
+ assert_eq!(parse("2021.1-dev-abc123"), parse("2021.1-dev-abc123"));
+ assert_ne!(parse("2021.1-dev-abc123"), parse("2021.1-dev-def123"));
+ }
+
+ #[test]
+ fn test_version_ordering_and_equality_dev() {
+ let v1 = parse("2021.3-dev-abc");
+ let v2 = parse("2021.3-dev-def");
+
+ // Exactly the same version are equal, but has no ordering
+ assert_eq!(v1, v1);
+ assert!(v1.partial_cmp(&v2).is_none());
+
+ // Equal down to the dev suffix are not equal, and has no ordering
+ assert_ne!(v1, v2);
+ assert!(v1.partial_cmp(&v2).is_none());
+ }
+
+ #[test]
fn test_parse() {
let version = "2021.34";
- let parsed = Version::parse(version);
- assert_eq!(parsed.year, "21");
- assert_eq!(parsed.incremental, "34");
- assert_eq!(parsed.alpha(), None);
- assert_eq!(parsed.beta(), None);
+ let parsed = parse(version);
+ assert_eq!(parsed.year, 2021);
+ assert_eq!(parsed.incremental, 34);
+ assert_eq!(alpha(&parsed), None);
+ assert_eq!(beta(&parsed), None);
assert_eq!(parsed.dev, None);
- assert!(parsed.is_stable());
+ assert!(is_stable(&parsed));
}
#[test]
fn test_parse_with_alpha() {
let version = "2023.1-alpha77";
- let parsed = Version::parse(version);
- assert_eq!(parsed.year, "23");
- assert_eq!(parsed.incremental, "1");
- assert_eq!(parsed.alpha(), Some("77"));
- assert_eq!(parsed.beta(), None);
+ let parsed = parse(version);
+ assert_eq!(parsed.year, 2023);
+ assert_eq!(parsed.incremental, 1);
+ assert_eq!(alpha(&parsed), Some(77));
+ assert_eq!(beta(&parsed), None);
assert_eq!(parsed.dev, None);
- assert!(!parsed.is_stable());
+ assert!(!is_stable(&parsed));
let version = "2021.34-alpha777";
- let parsed = Version::parse(version);
- assert_eq!(parsed.alpha(), Some("777"));
+ let parsed = parse(version);
+ assert_eq!(alpha(&parsed), Some(777));
}
#[test]
fn test_parse_with_beta() {
let version = "2021.34-beta5";
- let parsed = Version::parse(version);
- assert_eq!(parsed.year, "21");
- assert_eq!(parsed.incremental, "34");
- assert_eq!(parsed.alpha(), None);
- assert_eq!(parsed.beta(), Some("5"));
+ let parsed = parse(version);
+ assert_eq!(parsed.year, 2021);
+ assert_eq!(parsed.incremental, 34);
+ assert_eq!(alpha(&parsed), None);
+ assert_eq!(beta(&parsed), Some(5));
assert_eq!(parsed.dev, None);
- assert!(!parsed.is_stable());
+ assert!(!is_stable(&parsed));
let version = "2021.34-beta453";
- let parsed = Version::parse(version);
- assert_eq!(parsed.beta(), Some("453"));
+ let parsed = parse(version);
+ assert_eq!(beta(&parsed), Some(453));
}
#[test]
fn test_parse_with_dev() {
let version = "2021.34-dev-0b60e4d87";
- let parsed = Version::parse(version);
- assert_eq!(parsed.year, "21");
- assert_eq!(parsed.incremental, "34");
- assert!(!parsed.is_stable());
+ let parsed = parse(version);
+ assert_eq!(parsed.year, 2021);
+ assert_eq!(parsed.incremental, 34);
+ assert!(!is_stable(&parsed));
assert_eq!(parsed.dev, Some("0b60e4d87".to_string()));
- assert_eq!(parsed.alpha(), None);
- assert_eq!(parsed.beta(), None);
+ assert_eq!(alpha(&parsed), None);
+ assert_eq!(beta(&parsed), None);
}
#[test]
fn test_parse_both_beta_and_dev() {
let version = "2024.8-beta1-dev-e5483d";
- let parsed = Version::parse(version);
- assert_eq!(parsed.year, "24");
- assert_eq!(parsed.incremental, "8");
- assert_eq!(parsed.alpha(), None);
- assert_eq!(parsed.beta(), Some("1"));
+ let parsed = parse(version);
+ assert_eq!(parsed.year, 2024);
+ assert_eq!(parsed.incremental, 8);
+ assert_eq!(alpha(&parsed), None);
+ assert_eq!(beta(&parsed), Some(1));
assert_eq!(parsed.dev, Some("e5483d".to_string()));
- assert!(!parsed.is_stable());
+ assert!(!is_stable(&parsed));
+ }
+
+ #[test]
+ fn test_returns_error_on_invalid_version() {
+ assert!("2021".parse::<Version>().is_err());
+ assert!("not-a-version".parse::<Version>().is_err());
+ assert!("".parse::<Version>().is_err());
}
#[test]
- #[should_panic]
- fn test_panics_on_invalid_version() {
- Version::parse("2021");
+ fn test_returns_error_on_invalid_incremental() {
+ assert!("2021.2a".parse::<Version>().is_err());
}
#[test]
- #[should_panic]
- fn test_panics_on_invalid_version_type_number() {
- Version::parse("2021.1-beta001");
+ fn test_returns_error_on_invalid_version_type() {
+ assert!("2021.2-omega".parse::<Version>().is_err());
}
#[test]
- #[should_panic]
- fn test_panics_on_alpha_and_beta_in_same_version() {
- Version::parse("2021.1-beta5-alpha2");
+ fn test_returns_error_on_invalid_version_type_number() {
+ assert!("2021.1-beta001".parse::<Version>().is_err());
}
#[test]
- #[should_panic]
- fn test_panics_on_dev_without_commit_hash() {
- Version::parse("2021.1-dev");
+ fn test_returns_error_on_alpha_and_beta_in_same_version() {
+ assert!("2021.1-beta5-alpha2".parse::<Version>().is_err());
+ }
+
+ #[test]
+ fn test_returns_error_on_dev_without_commit_hash() {
+ assert!("2021.1-dev".parse::<Version>().is_err())
+ }
+
+ fn parse(version: &str) -> Version {
+ version.parse().unwrap()
}
#[test]
fn test_version_display() {
let version = "2024.8-beta1-dev-e5483d";
- let parsed = Version::parse(version);
+ let parsed: Version = version.parse().unwrap();
+
+ assert_eq!(format!("{parsed}"), version);
+
+ let version = "2024.8-beta1";
+ let parsed: Version = version.parse().unwrap();
assert_eq!(format!("{parsed}"), version);
+
+ let version = "2024.8-alpha77-dev-85483d";
+ let parsed: Version = version.parse().unwrap();
+
+ assert_eq!(format!("{parsed}"), version);
+
+ let version = "2024.12";
+ let parsed: Version = version.parse().unwrap();
+
+ assert_eq!(format!("{parsed}"), version);
+ }
+
+ fn alpha(version: &Version) -> Option<u32> {
+ match version.pre_stable {
+ Some(PreStableType::Alpha(alpha)) => Some(alpha),
+ _ => None,
+ }
+ }
+
+ fn beta(version: &Version) -> Option<u32> {
+ match version.pre_stable {
+ Some(PreStableType::Beta(beta)) => Some(beta),
+ _ => None,
+ }
+ }
+
+ fn is_stable(version: &Version) -> bool {
+ version.pre_stable.is_none() && !version.is_dev()
}
}
diff --git a/mullvad-version/src/main.rs b/mullvad-version/src/main.rs
index 1317260a2f..9d5cf6d152 100644
--- a/mullvad-version/src/main.rs
+++ b/mullvad-version/src/main.rs
@@ -62,51 +62,42 @@ fn to_semver(version: &str) -> String {
/// Version: 2021.34-dev
/// versionCode: 21349000
fn to_android_version_code(version: &str) -> String {
- let version = Version::parse(version);
+ let version: Version = version.parse().unwrap();
let (build_type, build_number) = if version.dev.is_some() {
- ("9", "000")
+ ("9", "000".to_string())
} else {
match &version.pre_stable {
- Some(PreStableType::Alpha(v)) => ("0", v.as_str()),
- Some(PreStableType::Beta(v)) => ("1", v.as_str()),
+ Some(PreStableType::Alpha(v)) => ("0", v.to_string()),
+ Some(PreStableType::Beta(v)) => ("1", v.to_string()),
// Stable version
- None => ("9", "000"),
+ None => ("9", "000".to_string()),
}
};
+ let year_last_two_digits = version.year % 100;
+
format!(
"{}{:0>2}{}{:0>3}",
- version.year, version.incremental, build_type, build_number,
+ year_last_two_digits, version.incremental, build_type, build_number,
)
}
fn to_windows_h_format(version_str: &str) -> String {
- let version = Version::parse(version_str);
- assert!(
- is_valid_windows_version(&version),
- "Invalid Windows version: {version:?}"
- );
+ let version = version_str.parse().unwrap();
let Version {
year, incremental, ..
} = version;
format!(
- "#define MAJOR_VERSION 20{year}
+ "#define MAJOR_VERSION {year}
#define MINOR_VERSION {incremental}
#define PATCH_VERSION 0
#define PRODUCT_VERSION \"{version_str}\""
)
}
-/// On Windows we currently support the following versions: stable, beta and dev.
-fn is_valid_windows_version(version: &Version) -> bool {
- version.is_stable()
- || version.beta().is_some()
- || (version.dev.is_some() && version.alpha().is_none())
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -132,8 +123,12 @@ mod tests {
}
#[test]
- #[should_panic]
- fn test_invalid_windows_version_code() {
- to_windows_h_format("2021.34-alpha1");
+ fn test_windows_version_h() {
+ let version_h = to_windows_h_format("2025.4-beta2-dev-abcdef");
+ let expected_version_h = "#define MAJOR_VERSION 2025
+#define MINOR_VERSION 4
+#define PATCH_VERSION 0
+#define PRODUCT_VERSION \"2025.4-beta2-dev-abcdef\"";
+ assert_eq!(expected_version_h, version_h);
}
}
diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs
index d640b26089..8c257d1899 100644
--- a/test/test-manager/src/tests/install.rs
+++ b/test/test-manager/src/tests/install.rs
@@ -1,4 +1,5 @@
use anyhow::{bail, ensure, Context};
+use std::str::FromStr;
use std::time::Duration;
use mullvad_management_interface::MullvadProxyClient;
@@ -109,8 +110,9 @@ pub async fn test_upgrade_app(
// Verify that the correct version was installed
let running_daemon_version = rpc.mullvad_daemon_version().await?;
- let running_daemon_version =
- mullvad_version::Version::parse(&running_daemon_version).to_string();
+ let running_daemon_version = mullvad_version::Version::from_str(&running_daemon_version)
+ .unwrap()
+ .to_string();
ensure!(
&TEST_CONFIG
.app_package_filename