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.
This commit is contained in:
Kalle Lindström 2025-02-05 13:08:20 +01:00 committed by Linus Färnstrand
parent 447ec20b79
commit 35fb1310a4
6 changed files with 304 additions and 316 deletions

View File

@ -5,10 +5,10 @@ use futures::{
FutureExt, SinkExt, StreamExt, TryFutureExt, FutureExt, SinkExt, StreamExt, TryFutureExt,
}; };
use mullvad_api::{availability::ApiAvailability, rest::MullvadRestHandle, AppVersionProxy}; 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 serde::{Deserialize, Serialize};
use std::{ use std::{
cmp::max,
future::Future, future::Future,
io, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -24,8 +24,8 @@ use tokio::{fs::File, io::AsyncReadExt};
const VERSION_INFO_FILENAME: &str = "version-info.json"; const VERSION_INFO_FILENAME: &str = "version-info.json";
static APP_VERSION: LazyLock<ParsedAppVersion> = static APP_VERSION: LazyLock<Version> =
LazyLock::new(|| ParsedAppVersion::from_str(mullvad_version::VERSION).unwrap()); LazyLock::new(|| Version::from_str(mullvad_version::VERSION).unwrap());
static IS_DEV_BUILD: LazyLock<bool> = LazyLock::new(|| APP_VERSION.is_dev()); static IS_DEV_BUILD: LazyLock<bool> = LazyLock::new(|| APP_VERSION.is_dev());
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(15); const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(15);
@ -535,26 +535,38 @@ fn dev_version_cache() -> AppVersionInfo {
suggested_upgrade: None, suggested_upgrade: None,
} }
} }
/// If current_version is not the latest, return a string containing the latest version. /// If current_version is not the latest, return a string containing the latest version.
fn suggested_upgrade( fn suggested_upgrade(
current_version: &ParsedAppVersion, current_version: &Version,
latest_stable: &Option<String>, latest_stable: &Option<String>,
latest_beta: &str, latest_beta: &str,
show_beta: bool, show_beta: bool,
) -> Option<String> { ) -> Option<String> {
let stable_version = latest_stable let stable_version = latest_stable
.as_ref() .as_ref()
.and_then(|stable| ParsedAppVersion::from_str(stable).ok()); .and_then(|stable| Version::from_str(stable).ok());
let beta_version = if show_beta { let beta_version = if show_beta {
ParsedAppVersion::from_str(latest_beta).ok() Version::from_str(latest_beta).ok()
} else { } else {
None 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()) Some(latest_version.to_string())
} else { } else {
None None
@ -736,13 +748,17 @@ mod test {
let latest_stable = Some("2020.4".to_string()); let latest_stable = Some("2020.4".to_string());
let latest_beta = "2020.5-beta3"; let latest_beta = "2020.5-beta3";
let older_stable = ParsedAppVersion::from_str("2020.3").unwrap(); let older_stable = Version::from_str("2020.3").unwrap();
let current_stable = ParsedAppVersion::from_str("2020.4").unwrap(); let current_stable = Version::from_str("2020.4").unwrap();
let newer_stable = ParsedAppVersion::from_str("2021.5").unwrap(); let newer_stable = Version::from_str("2021.5").unwrap();
let older_beta = ParsedAppVersion::from_str("2020.3-beta3").unwrap(); let older_beta = Version::from_str("2020.3-beta3").unwrap();
let current_beta = ParsedAppVersion::from_str("2020.5-beta3").unwrap(); let current_beta = Version::from_str("2020.5-beta3").unwrap();
let newer_beta = ParsedAppVersion::from_str("2021.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!( assert_eq!(
suggested_upgrade(&older_stable, &latest_stable, latest_beta, false), suggested_upgrade(&older_stable, &latest_stable, latest_beta, false),
@ -768,6 +784,7 @@ mod test {
suggested_upgrade(&newer_stable, &latest_stable, latest_beta, true), suggested_upgrade(&newer_stable, &latest_stable, latest_beta, true),
None None
); );
assert_eq!( assert_eq!(
suggested_upgrade(&older_beta, &latest_stable, latest_beta, false), suggested_upgrade(&older_beta, &latest_stable, latest_beta, false),
Some("2020.4".to_owned()) Some("2020.4".to_owned())
@ -792,5 +809,30 @@ mod test {
suggested_upgrade(&newer_beta, &latest_stable, latest_beta, true), suggested_upgrade(&newer_beta, &latest_stable, latest_beta, true),
None 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
);
} }
} }

View File

@ -1,15 +1,14 @@
use clap::Parser; 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_api::{proxy::ApiConnectionMode, ApiEndpoint, DEVICE_NOT_FOUND};
use mullvad_management_interface::MullvadProxyClient; 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_core::firewall::{self, Firewall};
use talpid_future::retry::{retry_future, ConstantInterval}; use talpid_future::retry::{retry_future, ConstantInterval};
use talpid_types::ErrorExt; use talpid_types::ErrorExt;
static APP_VERSION: LazyLock<ParsedAppVersion> = static APP_VERSION: LazyLock<Version> =
LazyLock::new(|| ParsedAppVersion::from_str(mullvad_version::VERSION).unwrap()); LazyLock::new(|| Version::from_str(mullvad_version::VERSION).unwrap());
const DEVICE_REMOVAL_STRATEGY: ConstantInterval = ConstantInterval::new(Duration::ZERO, Some(5)); 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> { fn is_older_version(old_version: &str) -> Result<ExitStatus, Error> {
let parsed_version = 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 ExitStatus::Ok
} else { } else {
ExitStatus::VersionNotOlder ExitStatus::VersionNotOlder

View File

@ -1,17 +1,4 @@
use regex::Regex;
use serde::{Deserialize, Serialize}; 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 /// AppVersionInfo represents the current stable and the current latest release versions of the
/// Mullvad VPN app. /// Mullvad VPN app.
@ -35,166 +22,3 @@ pub struct AppVersionInfo {
} }
pub type AppVersion = String; 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,);
}
}
}

View File

@ -1,8 +1,8 @@
use std::cmp::Ordering;
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::PreStableType::{Alpha, Beta};
use regex::Regex; use regex::Regex;
/// The Mullvad VPN app product version /// 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)] #[derive(Debug, Clone, PartialEq)]
pub struct Version { pub struct Version {
/// The last two digits of the version's year pub year: u32,
pub year: String, pub incremental: u32,
pub incremental: String, /// A version can have an optional pre-stable type, e.g. alpha or beta.
/// 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 pre_stable: Option<PreStableType>, pub pre_stable: Option<PreStableType>,
/// All versions may have an optional -dev-[commit hash] suffix. /// All versions may have an optional -dev-[commit hash] suffix.
pub dev: Option<String>, pub dev: Option<String>,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum PreStableType { pub enum PreStableType {
Alpha(String), Alpha(u32),
Beta(String), Beta(u32),
}
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,
}
}
}
impl PartialOrd for PreStableType {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
} }
impl Version { impl Version {
pub fn parse(version: &str) -> Version { /// Returns true if this version has a -dev suffix, e.g. 2025.2-beta1-dev-123abc
Version::from_str(version).unwrap() pub fn is_dev(&self) -> bool {
self.dev.is_some()
} }
}
pub fn is_stable(&self) -> bool { impl PartialOrd for Version {
self.pre_stable.is_none() && self.dev.is_none() 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)
}
};
pub fn alpha(&self) -> Option<&str> { // The dev vs non-dev ordering. For a version of a given type, if all else is equal
match &self.pre_stable { // a dev version is greater than a non-dev version.
Some(PreStableType::Alpha(v)) => Some(v), let dev_ordering = match (self.is_dev(), other.is_dev()) {
_ => None, (true, false) => Some(Ordering::Greater),
} (false, true) => Some(Ordering::Less),
} (_, _) => None,
};
pub fn beta(&self) -> Option<&str> { let release_ordering = self
match &self.pre_stable { .year
Some(PreStableType::Beta(beta)) => Some(beta), .cmp(&other.year)
_ => None, .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, dev,
} = &self; } = &self;
write!(f, "20{year}.{incremental}")?; write!(f, "{year}.{incremental}")?;
match pre_stable { match pre_stable {
Some(PreStableType::Alpha(version)) => write!(f, "-alpha{version}")?, Some(PreStableType::Alpha(version)) => write!(f, "-alpha{version}")?,
@ -76,14 +106,10 @@ impl Display for Version {
} }
} }
impl FromStr for Version { static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
type Err = String; Regex::new(
r"(?x) # enable insignificant whitespace mode
fn from_str(version: &str) -> Result<Self, Self::Err> { (?<year>\d{4})\. # the year
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
(?<incremental>[1-9]\d?) # the incrementing version number (?<incremental>[1-9]\d?) # the incrementing version number
(?: # (optional) alpha or beta or dev (?: # (optional) alpha or beta or dev
-alpha(?<alpha>[1-9]\d?\d?)| -alpha(?<alpha>[1-9]\d?\d?)|
@ -93,34 +119,35 @@ impl FromStr for Version {
-dev-(?<dev>[0-9a-f]+) -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 let captures = VERSION_REGEX
.captures(version) .captures(version)
.ok_or_else(|| format!("Version does not match expected format: {version}"))?; .ok_or_else(|| format!("Version does not match expected format: {version}"))?;
let year = captures let year = captures.name("year").unwrap().as_str().parse().unwrap();
.name("year")
.expect("Missing year")
.as_str()
.to_owned();
let incremental = captures let incremental = captures
.name("incremental") .name("incremental")
.ok_or("Missing incremental")? .unwrap()
.as_str() .as_str()
.to_owned(); .parse()
.unwrap();
let alpha = captures.name("alpha").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().to_owned()); 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 dev = captures.name("dev").map(|m| m.as_str().to_owned());
let pre_stable = match (alpha, beta) { let pre_stable = match (alpha, beta) {
(None, None) => None, (None, None) => None,
(Some(v), None) => Some(Alpha(v)), (Some(v), None) => Some(PreStableType::Alpha(v)),
(None, Some(v)) => Some(Beta(v)), (None, Some(v)) => Some(PreStableType::Beta(v)),
_ => return Err(format!("Invalid version: {version}")), _ => return Err(format!("Invalid version: {version}")),
}; };
@ -137,103 +164,202 @@ impl FromStr for Version {
mod tests { mod tests {
use super::*; 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] #[test]
fn test_parse() { fn test_parse() {
let version = "2021.34"; let version = "2021.34";
let parsed = Version::parse(version); let parsed = parse(version);
assert_eq!(parsed.year, "21"); assert_eq!(parsed.year, 2021);
assert_eq!(parsed.incremental, "34"); assert_eq!(parsed.incremental, 34);
assert_eq!(parsed.alpha(), None); assert_eq!(alpha(&parsed), None);
assert_eq!(parsed.beta(), None); assert_eq!(beta(&parsed), None);
assert_eq!(parsed.dev, None); assert_eq!(parsed.dev, None);
assert!(parsed.is_stable()); assert!(is_stable(&parsed));
} }
#[test] #[test]
fn test_parse_with_alpha() { fn test_parse_with_alpha() {
let version = "2023.1-alpha77"; let version = "2023.1-alpha77";
let parsed = Version::parse(version); let parsed = parse(version);
assert_eq!(parsed.year, "23"); assert_eq!(parsed.year, 2023);
assert_eq!(parsed.incremental, "1"); assert_eq!(parsed.incremental, 1);
assert_eq!(parsed.alpha(), Some("77")); assert_eq!(alpha(&parsed), Some(77));
assert_eq!(parsed.beta(), None); assert_eq!(beta(&parsed), None);
assert_eq!(parsed.dev, None); assert_eq!(parsed.dev, None);
assert!(!parsed.is_stable()); assert!(!is_stable(&parsed));
let version = "2021.34-alpha777"; let version = "2021.34-alpha777";
let parsed = Version::parse(version); let parsed = parse(version);
assert_eq!(parsed.alpha(), Some("777")); assert_eq!(alpha(&parsed), Some(777));
} }
#[test] #[test]
fn test_parse_with_beta() { fn test_parse_with_beta() {
let version = "2021.34-beta5"; let version = "2021.34-beta5";
let parsed = Version::parse(version); let parsed = parse(version);
assert_eq!(parsed.year, "21"); assert_eq!(parsed.year, 2021);
assert_eq!(parsed.incremental, "34"); assert_eq!(parsed.incremental, 34);
assert_eq!(parsed.alpha(), None); assert_eq!(alpha(&parsed), None);
assert_eq!(parsed.beta(), Some("5")); assert_eq!(beta(&parsed), Some(5));
assert_eq!(parsed.dev, None); assert_eq!(parsed.dev, None);
assert!(!parsed.is_stable()); assert!(!is_stable(&parsed));
let version = "2021.34-beta453"; let version = "2021.34-beta453";
let parsed = Version::parse(version); let parsed = parse(version);
assert_eq!(parsed.beta(), Some("453")); assert_eq!(beta(&parsed), Some(453));
} }
#[test] #[test]
fn test_parse_with_dev() { fn test_parse_with_dev() {
let version = "2021.34-dev-0b60e4d87"; let version = "2021.34-dev-0b60e4d87";
let parsed = Version::parse(version); let parsed = parse(version);
assert_eq!(parsed.year, "21"); assert_eq!(parsed.year, 2021);
assert_eq!(parsed.incremental, "34"); assert_eq!(parsed.incremental, 34);
assert!(!parsed.is_stable()); assert!(!is_stable(&parsed));
assert_eq!(parsed.dev, Some("0b60e4d87".to_string())); assert_eq!(parsed.dev, Some("0b60e4d87".to_string()));
assert_eq!(parsed.alpha(), None); assert_eq!(alpha(&parsed), None);
assert_eq!(parsed.beta(), None); assert_eq!(beta(&parsed), None);
} }
#[test] #[test]
fn test_parse_both_beta_and_dev() { fn test_parse_both_beta_and_dev() {
let version = "2024.8-beta1-dev-e5483d"; let version = "2024.8-beta1-dev-e5483d";
let parsed = Version::parse(version); let parsed = parse(version);
assert_eq!(parsed.year, "24"); assert_eq!(parsed.year, 2024);
assert_eq!(parsed.incremental, "8"); assert_eq!(parsed.incremental, 8);
assert_eq!(parsed.alpha(), None); assert_eq!(alpha(&parsed), None);
assert_eq!(parsed.beta(), Some("1")); assert_eq!(beta(&parsed), Some(1));
assert_eq!(parsed.dev, Some("e5483d".to_string())); assert_eq!(parsed.dev, Some("e5483d".to_string()));
assert!(!parsed.is_stable()); assert!(!is_stable(&parsed));
} }
#[test] #[test]
#[should_panic] fn test_returns_error_on_invalid_version() {
fn test_panics_on_invalid_version() { assert!("2021".parse::<Version>().is_err());
Version::parse("2021"); assert!("not-a-version".parse::<Version>().is_err());
assert!("".parse::<Version>().is_err());
} }
#[test] #[test]
#[should_panic] fn test_returns_error_on_invalid_incremental() {
fn test_panics_on_invalid_version_type_number() { assert!("2021.2a".parse::<Version>().is_err());
Version::parse("2021.1-beta001");
} }
#[test] #[test]
#[should_panic] fn test_returns_error_on_invalid_version_type() {
fn test_panics_on_alpha_and_beta_in_same_version() { assert!("2021.2-omega".parse::<Version>().is_err());
Version::parse("2021.1-beta5-alpha2");
} }
#[test] #[test]
#[should_panic] fn test_returns_error_on_invalid_version_type_number() {
fn test_panics_on_dev_without_commit_hash() { assert!("2021.1-beta001".parse::<Version>().is_err());
Version::parse("2021.1-dev"); }
#[test]
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] #[test]
fn test_version_display() { fn test_version_display() {
let version = "2024.8-beta1-dev-e5483d"; 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); 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()
}
} }

View File

@ -62,51 +62,42 @@ fn to_semver(version: &str) -> String {
/// Version: 2021.34-dev /// Version: 2021.34-dev
/// versionCode: 21349000 /// versionCode: 21349000
fn to_android_version_code(version: &str) -> String { 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() { let (build_type, build_number) = if version.dev.is_some() {
("9", "000") ("9", "000".to_string())
} else { } else {
match &version.pre_stable { match &version.pre_stable {
Some(PreStableType::Alpha(v)) => ("0", v.as_str()), Some(PreStableType::Alpha(v)) => ("0", v.to_string()),
Some(PreStableType::Beta(v)) => ("1", v.as_str()), Some(PreStableType::Beta(v)) => ("1", v.to_string()),
// Stable version // Stable version
None => ("9", "000"), None => ("9", "000".to_string()),
} }
}; };
let year_last_two_digits = version.year % 100;
format!( format!(
"{}{:0>2}{}{:0>3}", "{}{: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 { fn to_windows_h_format(version_str: &str) -> String {
let version = Version::parse(version_str); let version = version_str.parse().unwrap();
assert!(
is_valid_windows_version(&version),
"Invalid Windows version: {version:?}"
);
let Version { let Version {
year, incremental, .. year, incremental, ..
} = version; } = version;
format!( format!(
"#define MAJOR_VERSION 20{year} "#define MAJOR_VERSION {year}
#define MINOR_VERSION {incremental} #define MINOR_VERSION {incremental}
#define PATCH_VERSION 0 #define PATCH_VERSION 0
#define PRODUCT_VERSION \"{version_str}\"" #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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -132,8 +123,12 @@ mod tests {
} }
#[test] #[test]
#[should_panic] fn test_windows_version_h() {
fn test_invalid_windows_version_code() { let version_h = to_windows_h_format("2025.4-beta2-dev-abcdef");
to_windows_h_format("2021.34-alpha1"); 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);
} }
} }

View File

@ -1,4 +1,5 @@
use anyhow::{bail, ensure, Context}; use anyhow::{bail, ensure, Context};
use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use mullvad_management_interface::MullvadProxyClient; use mullvad_management_interface::MullvadProxyClient;
@ -109,8 +110,9 @@ pub async fn test_upgrade_app(
// Verify that the correct version was installed // Verify that the correct version was installed
let running_daemon_version = rpc.mullvad_daemon_version().await?; let running_daemon_version = rpc.mullvad_daemon_version().await?;
let running_daemon_version = let running_daemon_version = mullvad_version::Version::from_str(&running_daemon_version)
mullvad_version::Version::parse(&running_daemon_version).to_string(); .unwrap()
.to_string();
ensure!( ensure!(
&TEST_CONFIG &TEST_CONFIG
.app_package_filename .app_package_filename