diff options
| -rw-r--r-- | mullvad-api/src/version.rs | 4 | ||||
| -rw-r--r-- | mullvad-update/mullvad-release/src/main.rs | 15 | ||||
| -rw-r--r-- | mullvad-update/mullvad-release/src/platform.rs | 16 | ||||
| -rw-r--r-- | mullvad-update/src/format/mod.rs | 16 | ||||
| -rw-r--r-- | mullvad-update/src/version.rs | 105 |
5 files changed, 124 insertions, 32 deletions
diff --git a/mullvad-api/src/version.rs b/mullvad-api/src/version.rs index ff1ec63788..4e4fa1cb62 100644 --- a/mullvad-api/src/version.rs +++ b/mullvad-api/src/version.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use http::StatusCode; use http::header; -use mullvad_update::version::{VersionInfo, VersionParameters, is_version_supported}; +use mullvad_update::version::{Rollout, VersionInfo, VersionParameters, is_version_supported}; type AppVersion = String; @@ -107,7 +107,7 @@ impl AppVersionProxy { &self, platform: &str, architecture: mullvad_update::format::Architecture, - rollout: f32, + rollout: Rollout, lowest_metadata_version: usize, platform_version: Option<String>, etag: Option<String>, diff --git a/mullvad-update/mullvad-release/src/main.rs b/mullvad-update/mullvad-release/src/main.rs index f5d6dd9181..d9bc523aca 100644 --- a/mullvad-update/mullvad-release/src/main.rs +++ b/mullvad-update/mullvad-release/src/main.rs @@ -15,7 +15,7 @@ use platform::Platform; use mullvad_update::{ api::HttpVersionInfoProvider, format::{self, SignedResponse, key}, - version::Rollout, + version::{FULLY_ROLLED_OUT, Rollout}, }; use crate::io_util::wait_for_confirm; @@ -30,7 +30,7 @@ mod platform; const DEFAULT_EXPIRY_MONTHS: usize = 6; /// Rollout to use when not specified -const DEFAULT_ROLLOUT: f32 = 1.; +const DEFAULT_ROLLOUT: Rollout = FULLY_ROLLED_OUT; /// Filename for latest.json metadata const LATEST_FILENAME: &str = "latest.json"; @@ -76,9 +76,9 @@ pub enum Opt { version: mullvad_version::Version, /// Platforms to add releases for. All if none are specified platforms: Vec<Platform>, - /// Rollout fraction 0-1. The default is 1, i.e. 100% + /// Rollout fraction to set (0 = not rolled out, 1 = fully rolled out). #[arg(long, default_value_t = DEFAULT_ROLLOUT)] - rollout: f32, + rollout: Rollout, }, /// Remove release from `work/` @@ -95,9 +95,9 @@ pub enum Opt { version: mullvad_version::Version, /// Platforms to remove releases for. All if none are specified platforms: Vec<Platform>, - /// Rollout percentage. The default is 1 + /// If set, modify the rollout fraction. #[arg(long)] - rollout: Option<f32>, + rollout: Option<Rollout>, }, /// Sign using an ed25519 key and output the signed metadata to `signed/` @@ -124,11 +124,12 @@ pub enum Opt { QueryLatest { /// Platforms to query for. All if none are specified platforms: Vec<Platform>, - /// Rollout threshold to use (.0 = not rolled out, 1 = fully rolled out). + /// Rollout threshold to use (0 = not rolled out, 1 = fully rolled out). /// /// By default, any non-zero rollout is accepted. /// Setting the value to zero will also show supported versions that have /// been released but are currently not being rolled out. + // TODO: this prints 0%, but should print 1.1920929e-7 #[arg(long, default_value_t = mullvad_update::version::SUPPORTED_VERSION)] rollout: Rollout, }, diff --git a/mullvad-update/mullvad-release/src/platform.rs b/mullvad-update/mullvad-release/src/platform.rs index 3ebfcaaf37..d7114e484a 100644 --- a/mullvad-update/mullvad-release/src/platform.rs +++ b/mullvad-update/mullvad-release/src/platform.rs @@ -4,7 +4,9 @@ use anyhow::{Context, anyhow, bail}; use mullvad_update::{ api::{HttpVersionInfoProvider, MetaRepositoryPlatform}, format::{self, key}, - version::{MIN_VERIFY_METADATA_VERSION, VersionArchitecture, VersionInfo, VersionParameters}, + version::{ + MIN_VERIFY_METADATA_VERSION, Rollout, VersionArchitecture, VersionInfo, VersionParameters, + }, }; use std::{ cmp::Ordering, @@ -226,7 +228,7 @@ impl Platform { version: &mullvad_version::Version, changes: &str, base_urls: &[String], - rollout: f32, + rollout: Rollout, ) -> anyhow::Result<()> { let installers = self.installers(version, base_urls).await?; @@ -356,7 +358,7 @@ impl Platform { pub async fn modify_release( &self, version: &mullvad_version::Version, - rollout: Option<f32>, + rollout: Option<Rollout>, ) -> anyhow::Result<()> { let work_path = self.work_path(); println!("Modifying {version} in {}", work_path.display()); @@ -388,7 +390,7 @@ impl Platform { } /// Return the latest release for platforms in `signed/` - pub async fn query_latest(&self, rollout: f32) -> anyhow::Result<VersionQueryOutput> { + pub async fn query_latest(&self, rollout: Rollout) -> anyhow::Result<VersionQueryOutput> { let response = self.read_signed().await?; // Grab version info for all architectures @@ -477,10 +479,8 @@ fn print_release_info(release: &format::Release) { let architectures = architectures.join(", "); println!( - "- {} ({}) ({}%)", - release.version, - architectures, - (release.rollout * 100.) as u32 + "- {} ({}) ({})", + release.version, architectures, release.rollout, ); } diff --git a/mullvad-update/src/format/mod.rs b/mullvad-update/src/format/mod.rs index 790f38ee4b..d82537f49e 100644 --- a/mullvad-update/src/format/mod.rs +++ b/mullvad-update/src/format/mod.rs @@ -15,6 +15,8 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; +use crate::version::{FULLY_ROLLED_OUT, Rollout}; + pub mod deserializer; pub mod key; #[cfg(feature = "sign")] @@ -74,7 +76,7 @@ pub struct Release { /// Fraction of users that should receive the new version #[serde(default = "complete_rollout")] #[serde(skip_serializing_if = "is_complete_rollout")] - pub rollout: f32, + pub rollout: Rollout, } impl PartialEq for Release { @@ -90,12 +92,14 @@ impl PartialOrd for Release { } /// A full rollout includes all users -fn complete_rollout() -> f32 { - 1. +fn complete_rollout() -> Rollout { + FULLY_ROLLED_OUT } -fn is_complete_rollout(b: impl std::borrow::Borrow<f32>) -> bool { - (b.borrow() - complete_rollout()).abs() < f32::EPSILON +fn is_complete_rollout(b: impl std::borrow::Borrow<Rollout>) -> bool { + // TODO: do we actually need this? if so, should we bake it into Rollout::eq? + //(b.borrow() - complete_rollout()).abs() < f32::EPSILON + b.borrow() == &FULLY_ROLLED_OUT } /// App installer @@ -171,7 +175,7 @@ mod test { ); // rollout *should* be serialized if not equal to default value - let rollout = 0.99; + let rollout = Rollout::try_from(0.99).unwrap(); let serialized = serde_json::to_value(Release { version: "2024.1".parse().unwrap(), changelog: "".to_owned(), diff --git a/mullvad-update/src/version.rs b/mullvad-update/src/version.rs index 0fe6d7d66b..b8e95b7bda 100644 --- a/mullvad-update/src/version.rs +++ b/mullvad-update/src/version.rs @@ -4,11 +4,17 @@ //! //! The main input here is [VersionParameters], and the main output is [VersionInfo]. -use std::cmp::Ordering; +use std::{ + cmp::Ordering, + fmt::{self, Display}, + ops::RangeInclusive, + str::FromStr, +}; -use anyhow::Context; +use anyhow::{Context, bail}; use itertools::Itertools; use mullvad_version::PreStableType; +use serde::{Deserialize, Serialize, de::Error}; use crate::format::{self, Installer, Response}; @@ -30,17 +36,22 @@ pub struct VersionParameters { } /// Rollout threshold. Any version in the response below this threshold will be ignored -pub type Rollout = f32; +/// +/// INVARIANT: The inner f32 must be in the `VALID_ROLLOUT` range. +#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] +pub struct Rollout(f32); /// Accept *any* version (rollout >= 0) when querying for app info. -pub const IGNORE: Rollout = 0.; +pub const IGNORE: Rollout = Rollout(0.); /// Accept any version (rollout > 0) when querying for app info. /// Only versions with a non-zero rollout are supported. -pub const SUPPORTED_VERSION: Rollout = f32::EPSILON; +pub const SUPPORTED_VERSION: Rollout = Rollout(f32::EPSILON); /// Accept only fully rolled out versions (rollout >= 1) when querying for app info. -pub const FULLY_ROLLED_OUT: Rollout = 1.; +pub const FULLY_ROLLED_OUT: Rollout = Rollout(1.); + +pub const VALID_ROLLOUT: RangeInclusive<f32> = 0.0..=1.0; /// Installer architecture pub type VersionArchitecture = format::Architecture; @@ -153,6 +164,65 @@ pub fn is_version_supported( .any(|release| release.version.eq(¤t_version)) } +impl TryFrom<f32> for Rollout { + type Error = anyhow::Error; + + fn try_from(rollout: f32) -> Result<Self, Self::Error> { + if !rollout.is_finite() { + bail!("rollout value must be a finite number, but was {rollout}"); + } + + if !VALID_ROLLOUT.contains(&rollout) { + bail!( + "rollout value {rollout} is outside valid range {}..={}", + VALID_ROLLOUT.start(), + VALID_ROLLOUT.end(), + ); + } + + Ok(Rollout(rollout)) + } +} + +// TODO: the mullvad-release cli might rely on this being formatted as an f32 +impl Display for Rollout { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}%", (self.0 * 100.) as u32) + } +} + +// TODO: the mullvad-release cli might rely on this being formatted as an f32 +impl FromStr for Rollout { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let rollout: f32 = s.parse()?; + Rollout::try_from(rollout) + } +} + +impl<'de> Deserialize<'de> for Rollout { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let rollout = f32::deserialize(deserializer)?; + + Rollout::try_from(rollout) + .map_err(|e| e.to_string()) + .map_err(D::Error::custom) + } +} + +impl Serialize for Rollout { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + #[cfg(test)] mod test { use std::str::FromStr; @@ -174,7 +244,7 @@ mod test { let params = VersionParameters { architecture: VersionArchitecture::X86, - rollout: 1., + rollout: FULLY_ROLLED_OUT, allow_empty: false, lowest_metadata_version: 0, }; @@ -196,7 +266,7 @@ mod test { let params = VersionParameters { architecture: VersionArchitecture::Arm64, - rollout: 0.01, + rollout: SUPPORTED_VERSION, allow_empty: false, lowest_metadata_version: 0, }; @@ -218,7 +288,7 @@ mod test { let params = VersionParameters { architecture: VersionArchitecture::X86, - rollout: 0.01, + rollout: SUPPORTED_VERSION, allow_empty: true, lowest_metadata_version: 0, }; @@ -287,4 +357,21 @@ mod test { Ok(()) } + + const BAD_ROLLOUT_EXAMPLES: &[f32] = &[ + -f32::EPSILON, + 1.0 + f32::EPSILON, + f32::NAN, + f32::INFINITY, + f32::NEG_INFINITY, + ]; + + #[test] + fn test_rollout_deserialize_bad() { + for &bad_rollout in BAD_ROLLOUT_EXAMPLES { + let rollout_str = bad_rollout.to_string(); + serde_json::from_str::<Rollout>(&rollout_str) + .expect_err("must fail to deserialize bad rollout"); + } + } } |
