diff --git a/Cargo.lock b/Cargo.lock index 197184f..ec499cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "aster" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258989846dd255a1e0eeef92d425d345477c9999433cecc9f0879f4549d5e5c9" + [[package]] name = "async-trait" version = "0.1.89" @@ -1028,9 +1034,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fiat-crypto" @@ -1863,6 +1869,7 @@ name = "iota-util" version = "0.1.0" dependencies = [ "aes-gcm", + "anyhow", "base64", "hex", "hkdf", @@ -1872,8 +1879,12 @@ dependencies = [ "rand_core 0.6.4", "ratatui", "reqwest", + "semver", + "serde", + "serde_macros", "sha2", "sysinfo", + "tempfile", "tokio", "ttp-core", "ttp-native", @@ -2810,6 +2821,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quasi" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a532453b931a4483a5b2e40f0fe04aee35b6bc2c0eeec876f1bd2358a134d3" + +[[package]] +name = "quasi_codegen" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb4a9a5410fdbdacbeda8063ddb8add9838dfd4cf50ac486db98abb762d8bd6" +dependencies = [ + "aster", +] + +[[package]] +name = "quasi_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc2b36285ea5e54e4e267f83896267ff8c5aba4f66b2e7d186ed6d968f3715f" +dependencies = [ + "quasi_codegen", +] + [[package]] name = "quinn" version = "0.11.9" @@ -3375,6 +3410,24 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_codegen" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da68810d845f8e33a80243c28794650397056cbe7aea4c9c7516f55d1061c94e" +dependencies = [ + "aster", + "quasi", + "quasi_macros", + "serde_codegen_internals", +] + +[[package]] +name = "serde_codegen_internals" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b0115c5c602e81c61b787fb0f0fa76a614f8dbe9100b2b59b7d590155672c80" + [[package]] name = "serde_core" version = "1.0.228" @@ -3408,6 +3461,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_macros" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3cf1c01933271e1e72bb788e0499d1bca8af2c09efcc3ddc0b04ff22d080b83" +dependencies = [ + "serde_codegen", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4010,7 +4072,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttp-core" version = "0.1.0" -source = "git+https://git.methanium.net/Tensamin/TTP.git#bacbcb0e38791cf1a38cf5851beb20d5645002ba" +source = "git+https://git.methanium.net/Tensamin/TTP.git#2322bdced8183e970405d5017cd87ba805309a4c" dependencies = [ "base64", "byteorder", @@ -4022,7 +4084,7 @@ dependencies = [ [[package]] name = "ttp-native" version = "0.1.0" -source = "git+https://git.methanium.net/Tensamin/TTP.git#bacbcb0e38791cf1a38cf5851beb20d5645002ba" +source = "git+https://git.methanium.net/Tensamin/TTP.git#2322bdced8183e970405d5017cd87ba805309a4c" dependencies = [ "quinn", "rustls", diff --git a/iota-core/src/main.rs b/iota-core/src/main.rs index 67f72e2..209518f 100644 --- a/iota-core/src/main.rs +++ b/iota-core/src/main.rs @@ -1,4 +1,5 @@ mod consent_state; +use iota_util::update_util::check_update; use pnet::datalink::NetworkInterface; use tokio::time::{Duration, sleep}; @@ -52,6 +53,7 @@ async fn main() { println!("You can find this at 'agreements'!"); return; } + check_update(); iota_state::setup(); let main_screen = MainScreen::new(ui.clone()).await; @@ -118,7 +120,7 @@ async fn main() { } } } - /* Community port activation is used for activating the port for communities. + /* Community port activation is used for activating the port for communities. * Code is currently commented because communities have not been implemented yet. if start(port).await { log_t!("community_active", ip, port.to_string()); diff --git a/iota-util/Cargo.toml b/iota-util/Cargo.toml index 9a1d4cb..41502e1 100644 --- a/iota-util/Cargo.toml +++ b/iota-util/Cargo.toml @@ -25,3 +25,8 @@ x448 = { version = "*" } hkdf = "0.12.4" once_cell = "1.21.3" hex = "*" +serde = "1.0.228" +tempfile = "3.27.0" +anyhow = "1.0.102" +semver = "1.0.28" +serde_macros = "0.8.9" diff --git a/iota-util/src/lib.rs b/iota-util/src/lib.rs index d104f80..ebef516 100644 --- a/iota-util/src/lib.rs +++ b/iota-util/src/lib.rs @@ -2,3 +2,4 @@ pub mod crypto_helper; pub mod crypto_util; pub mod file_util; pub mod langu; +pub mod update_util; diff --git a/iota-util/src/update_util.rs b/iota-util/src/update_util.rs new file mode 100644 index 0000000..2752008 --- /dev/null +++ b/iota-util/src/update_util.rs @@ -0,0 +1,134 @@ +/* This file is used for the auto update function for the Iota. + * It connects to the git server from methanium and checks if + * the version has updated inside the cargo.toml file. + * It is made by Yolokit and pasted in by AlexEmmet */ + +use anyhow::{Context, Result, anyhow}; +use semver::Version; +use serde::Deserialize; +use std::fs::File; +use std::io::copy; +use tempfile::NamedTempFile; + +const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const API_BASE: &str = "https://git.methanium.net/api/v1"; +const OWNER: &str = "Tensamin"; +const REPO: &str = "Iota"; + +#[derive(Debug, Deserialize)] +struct Release { + tag_name: String, + assets: Vec, +} + +#[derive(Debug, Deserialize)] +struct Asset { + name: String, + browser_download_url: String, +} + +async fn latest_release() -> Result { + let url = format!("{API_BASE}/repos/{OWNER}/{REPO}/releases/latest"); + + let response = reqwest::get(&url) + .await + .context("failed to query latest release.")?; + + if !response.status().is_success() { + return Err(anyhow!("release API returned {}", response.status())); + } + + Ok(response + .json() + .await + .context("failed to parse release JSON")?) +} + +async fn parse_tag_version(tag: &str) -> Result { + let normalized = tag.strip_prefix('v').unwrap_or(tag); + Ok(Version::parse(normalized)?) +} + +async fn current_version() -> Result { + Ok(Version::parse(CURRENT_VERSION)?) +} + +async fn asset_name_for_current_platform() -> String { + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + + match (os, arch) { + ("linux", "x86_64") => "iota-linux-x86_64".to_string(), + ("linux", "aarch64") => "iota-linux-aarch64".to_string(), + ("windows", "x86_64") => "iota-windows-x86_64.exe".to_string(), + ("macos", "x86_64") => "iota-macos-x86_64".to_string(), + ("macos", "aarch64") => "iota-macos-aarch64".to_string(), + _ => panic!("unsupported platform: {os}/{arch}"), + } +} + +async fn download_asset(url: &str) -> Result { + let mut response = reqwest::get(url) + .await + .context("failed to download asset")?; + + if !response.status().is_success() { + return Err(anyhow!("asset download returned {}", response.status())); + } + + let tmp = NamedTempFile::new().context("failed to create temp file")?; + let mut out = File::create(tmp.path()).context("failed to open temp file")?; + + let bytes = response + .bytes() + .await + .context("failed to read response bytes")?; + + std::fs::write(tmp.path(), &bytes).context("failed to write file")?; + + Ok(tmp) +} + +async fn check_for_update() -> Result> { + let current = current_version().await?; + let release = latest_release().await?; + let latest = parse_tag_version(&release.tag_name).await?; + + if latest > current { + Ok(Some(release)) + } else { + Ok(None) + } +} + +async fn perform_update() -> Result { + let Some(release) = check_for_update().await? else { + return Ok(false); + }; + + let wanted_asset = asset_name_for_current_platform().await; + + let asset = release + .assets + .iter() + .find(|a| a.name == wanted_asset) + .ok_or_else(|| anyhow!("no matching asset found: {}", wanted_asset))?; + + log!("Downloading update: {}", asset.name); + + let downloaded = download_asset(&asset.browser_download_url).await?; + + self_replace::self_replace(downloaded.path()) + .context("failed to replace current executable")?; + + Ok(true) +} + +pub async fn check_update() -> Result { + if perform_update().await? { + return Ok(true); + } else { + return Ok(false); + } +}