use std::time::Duration;
const WORKER_URL: &str = "https://tokensave-counter.enzinol.workers.dev";
const GITHUB_RELEASES_URL: &str =
"https://api.github.com/repos/aovestdipaperino/tokensave/releases/latest";
const GITHUB_RELEASES_LIST_URL: &str =
"https://api.github.com/repos/aovestdipaperino/tokensave/releases?per_page=10";
const FLUSH_TIMEOUT: Duration = Duration::from_secs(2);
const FETCH_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(serde::Deserialize)]
struct WorkerResponse {
total: u64,
}
pub fn agent_with_timeout(timeout: Duration) -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_global(Some(timeout))
.build()
.into()
}
pub fn flush_pending(amount: u64) -> Option<u64> {
if amount == 0 {
return None;
}
let body = serde_json::json!({ "amount": amount });
let agent = agent_with_timeout(FLUSH_TIMEOUT);
let parsed: WorkerResponse = agent
.post(&format!("{WORKER_URL}/increment"))
.send_json(&body)
.ok()?
.body_mut()
.read_json()
.ok()?;
Some(parsed.total)
}
pub fn fetch_worldwide_total() -> Option<u64> {
let agent = agent_with_timeout(FETCH_TIMEOUT);
let parsed: WorkerResponse = agent
.get(&format!("{WORKER_URL}/total"))
.call()
.ok()?
.body_mut()
.read_json()
.ok()?;
Some(parsed.total)
}
#[derive(serde::Deserialize)]
struct CountriesResponse {
flags: Vec<String>,
}
pub fn fetch_country_flags() -> Vec<String> {
let agent = agent_with_timeout(Duration::from_millis(500));
let Ok(mut resp) = agent.get(&format!("{WORKER_URL}/countries")).call() else {
return Vec::new();
};
let Ok(parsed): Result<CountriesResponse, _> = resp.body_mut().read_json() else {
return Vec::new();
};
parsed.flags
}
#[derive(serde::Deserialize)]
struct GitHubRelease {
tag_name: String,
#[serde(default)]
prerelease: bool,
#[serde(default)]
assets: Vec<GitHubAsset>,
}
#[derive(serde::Deserialize)]
struct GitHubAsset {
name: String,
}
pub(crate) fn current_platform() -> &'static str {
if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
"aarch64-macos"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
"x86_64-macos"
} else if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
"x86_64-linux"
} else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
"aarch64-linux"
} else if cfg!(target_os = "windows") {
"x86_64-windows"
} else {
"unknown"
}
}
pub(crate) fn asset_name(version: &str, is_beta: bool) -> String {
let prefix = if is_beta {
"tokensave-beta"
} else {
"tokensave"
};
let platform = current_platform();
let ext = if cfg!(windows) { "zip" } else { "tar.gz" };
format!("{prefix}-v{version}-{platform}.{ext}")
}
fn release_has_current_platform_asset(release: &GitHubRelease) -> bool {
let version = release.tag_name.trim_start_matches('v');
let expected = asset_name(version, release.prerelease);
release.assets.iter().any(|a| a.name == expected)
}
pub fn fetch_latest_version() -> Option<String> {
if is_beta() {
fetch_latest_beta_version()
} else {
fetch_latest_stable_version()
}
}
pub fn fetch_latest_stable_version() -> Option<String> {
let agent = agent_with_timeout(FETCH_TIMEOUT);
let release: GitHubRelease = agent
.get(GITHUB_RELEASES_URL)
.header("User-Agent", "tokensave")
.call()
.ok()?
.body_mut()
.read_json()
.ok()?;
if !release_has_current_platform_asset(&release) {
return None;
}
Some(release.tag_name.trim_start_matches('v').to_string())
}
pub fn fetch_latest_beta_version() -> Option<String> {
let agent = agent_with_timeout(FETCH_TIMEOUT);
let releases: Vec<GitHubRelease> = agent
.get(GITHUB_RELEASES_LIST_URL)
.header("User-Agent", "tokensave")
.call()
.ok()?
.body_mut()
.read_json()
.ok()?;
releases
.into_iter()
.find(|r| r.prerelease && release_has_current_platform_asset(r))
.map(|r| r.tag_name.trim_start_matches('v').to_string())
}
pub fn is_beta() -> bool {
env!("CARGO_PKG_VERSION").contains('-')
}
pub fn is_newer_version(current: &str, latest: &str) -> bool {
fn parse(v: &str) -> Option<(u64, u64, u64, Option<&str>)> {
let (base, pre) = match v.split_once('-') {
Some((b, p)) => (b, Some(p)),
None => (v, None),
};
let mut parts = base.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next()?.parse().ok()?;
Some((major, minor, patch, pre))
}
match (parse(current), parse(latest)) {
(Some((cm, cn, cp, cpre)), Some((lm, ln, lp, lpre))) => {
if cpre.is_some() != lpre.is_some() {
return false;
}
let c_base = (cm, cn, cp);
let l_base = (lm, ln, lp);
if l_base != c_base {
return l_base > c_base;
}
match (cpre, lpre) {
(Some(a), Some(b)) => b > a,
_ => false,
}
}
_ => false,
}
}
pub fn is_newer_minor_version(current: &str, latest: &str) -> bool {
fn parse(v: &str) -> Option<(u64, u64)> {
let base = v.split_once('-').map_or(v, |(b, _)| b);
let mut parts = base.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
Some((major, minor))
}
is_newer_version(current, latest)
&& match (parse(current), parse(latest)) {
(Some(c), Some(l)) => l > c,
_ => true,
}
}
pub enum InstallMethod {
Cargo,
Brew,
Scoop,
Unknown,
}
pub fn detect_install_method() -> InstallMethod {
let Ok(exe) = std::env::current_exe() else {
return InstallMethod::Unknown;
};
let path = exe.to_string_lossy();
if path.contains(".cargo/bin") || path.contains(".cargo\\bin") {
InstallMethod::Cargo
} else if path.contains("/homebrew/") || path.contains("/Cellar/") {
InstallMethod::Brew
} else if path.contains("\\scoop\\") || path.contains("/scoop/") {
InstallMethod::Scoop
} else {
InstallMethod::Unknown
}
}
pub fn upgrade_command(_method: &InstallMethod) -> &'static str {
"tokensave upgrade"
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn release(tag: &str, prerelease: bool, asset_names: &[&str]) -> GitHubRelease {
GitHubRelease {
tag_name: tag.to_string(),
prerelease,
assets: asset_names
.iter()
.map(|n| GitHubAsset {
name: (*n).to_string(),
})
.collect(),
}
}
#[test]
fn skips_release_with_no_assets() {
let r = release("v9.9.9", false, &[]);
assert!(!release_has_current_platform_asset(&r));
}
#[test]
fn skips_release_missing_current_platform_asset() {
let r = release(
"v9.9.9",
false,
&[
"tokensave-v9.9.9-some-other-platform.tar.gz",
"tokensave-v9.9.9-yet-another-platform.tar.gz",
],
);
assert!(!release_has_current_platform_asset(&r));
}
#[test]
fn accepts_release_with_matching_asset() {
let expected = asset_name("9.9.9", false);
let r = release("v9.9.9", false, &[&expected]);
assert!(release_has_current_platform_asset(&r));
}
#[test]
fn accepts_beta_release_with_matching_beta_asset() {
let expected = asset_name("9.9.9-beta.1", true);
let r = release("v9.9.9-beta.1", true, &[&expected]);
assert!(release_has_current_platform_asset(&r));
}
#[test]
fn rejects_stable_named_asset_on_beta_release() {
let stable_name = asset_name("9.9.9-beta.1", false);
let r = release("v9.9.9-beta.1", true, &[&stable_name]);
assert!(!release_has_current_platform_asset(&r));
}
}