summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--mullvad-api/src/version.rs4
-rw-r--r--mullvad-update/mullvad-release/src/main.rs15
-rw-r--r--mullvad-update/mullvad-release/src/platform.rs16
-rw-r--r--mullvad-update/src/format/mod.rs16
-rw-r--r--mullvad-update/src/version.rs105
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(&current_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");
+ }
+ }
}