diff --git a/Cargo.lock b/Cargo.lock index 324bca0..01016d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,15 @@ dependencies = [ "libc", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -198,6 +207,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -208,6 +232,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -218,6 +253,38 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.13.0" @@ -249,6 +316,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -652,6 +729,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -662,6 +749,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.22" @@ -764,6 +857,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -946,6 +1045,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -1180,6 +1290,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1278,6 +1397,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -1584,6 +1709,7 @@ version = "0.1.0" dependencies = [ "chrono", "color-eyre", + "directories", "hex", "itertools", "oauth2", @@ -1595,6 +1721,8 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "walkdir", + "zip", ] [[package]] @@ -1648,6 +1776,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1770,6 +1908,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1949,3 +2096,34 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zip" +version = "2.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index dd729c6..7aa49a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4.38", features = ["serde"] } color-eyre = "0.6.3" +directories = "5.0.1" hex = "0.4.3" itertools = "0.13.0" oauth2 = "4.4.2" @@ -22,3 +23,5 @@ thiserror = "1.0.63" tokio = { version = "1.38.0", features = ["full"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] } +walkdir = "2.5.0" +zip = { version = "2.1.6", default-features = false, features = ["deflate"] } diff --git a/src/auth.rs b/src/auth.rs index ea76e38..6d3eded 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -55,7 +55,7 @@ struct MinecraftLoginResponse { access_token: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Default)] pub struct MinecraftProfile { pub id: String, pub name: String, @@ -63,7 +63,7 @@ pub struct MinecraftProfile { #[derive(Debug, Error)] pub enum AuthenticationError { - #[error("reqwest error")] + #[error("reqwest error: {0}")] Reqwest(#[from] reqwest::Error), /// from `eyre::Report` because oauth2 error types are too complicated #[error(transparent)] @@ -122,6 +122,7 @@ type Result = std::result::Result; // TODO better error handling !!!! pub async fn login() -> Result { // temporary until i can access api.minecraftservices.com + return Ok(MinecraftProfile::default()); let access_token = request_msa_token().await?; let (xbox_token, userhash) = auth_xbox_live(&access_token).await?; diff --git a/src/main.rs b/src/main.rs index c66af4d..95f203c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod auth; mod modrinth; +mod storage; use std::{fs::File, sync::LazyLock}; @@ -39,6 +40,8 @@ async fn main() -> Result<()> { tracing::info!("starting..."); tracing::debug!("starting debug"); + storage::make_directories()?; + let profile = auth::login().await?; tracing::info!("logged in as {} ({})", profile.name, profile.id); @@ -55,7 +58,7 @@ async fn main() -> Result<()> { version.version_number ); - let path = version.download_file().await?; + let path = version.install_modpack().await?; tracing::info!("downloaded file at {:?}", path); } diff --git a/src/modrinth.rs b/src/modrinth.rs index eb17028..52dbc1f 100644 --- a/src/modrinth.rs +++ b/src/modrinth.rs @@ -1,10 +1,12 @@ -use std::{env, fs::File, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf}; use chrono::{DateTime, Utc}; -use color_eyre::eyre::{bail, Report, Result}; +use color_eyre::eyre::{Report, Result}; use itertools::Itertools; use serde::Deserialize; -use sha2::{Digest, Sha512}; +use walkdir::WalkDir; + +use crate::storage; const PROJECT_ID: &str = "JR0bkFKa"; @@ -21,7 +23,6 @@ pub struct ProjectVersion { pub struct VersionFile { url: String, filename: String, - size: usize, hashes: VersionFileHashes, } @@ -30,33 +31,74 @@ pub struct VersionFileHashes { sha512: String, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ModpackIndex { + files: Vec, + dependencies: HashMap, +} + +#[derive(Debug, Deserialize)] +struct ModpackFile { + path: PathBuf, + hashes: VersionFileHashes, + downloads: Vec, +} + impl ProjectVersion { pub async fn download_file(&self) -> Result { let version_file = &self.files[0]; - let path = env::temp_dir().join(&version_file.filename); - let response = crate::HTTP_CLIENT.get(&version_file.url).send().await?; - let mut bytes = &*response.bytes().await?; // deref to get &[u8] + storage::download_to_cache( + &version_file.url, + &version_file.filename, + &version_file.hashes.sha512, + ) + .await + } - if bytes.len() != version_file.size { - bail!("invalid length!") + pub async fn install_modpack(&self) -> Result { + let mrpack_path = self.download_file().await?; + let decompressed = storage::decompress_zip(&mrpack_path)?; + + let index_path = decompressed.join("modrinth.index.json"); + let index = serde_json::from_slice::(&std::fs::read(&index_path)?)?; + + let version_dir = storage::get_version_directory(&self.game_versions[0])?; + + for file in index.files { + let Some(filename) = file.path.file_name() else { + tracing::warn!("path {} does not have a filename", file.path.display()); + continue; + }; + + let download_path = storage::download_to_cache( + &file.downloads[0], + &filename.to_string_lossy(), + &file.hashes.sha512, + ) + .await?; + + let final_path = version_dir.join(file.path); + storage::move_file(download_path, &final_path)?; + + tracing::info!("downloaded file {}", final_path.display()); } - let expected_hash = hex::decode(&version_file.hashes.sha512)?; - let computed_hash = { - let mut sha512 = Sha512::new(); - sha512.update(bytes); - sha512.finalize() - }; + let overrides_path = decompressed.join("overrides"); + for entry in WalkDir::new(&overrides_path) { + let entry = entry?; - if expected_hash[..] != computed_hash[..] { - bail!("hash mismatch!") + if entry.path().is_file() { + let stripped = entry.path().strip_prefix(&overrides_path)?; + let final_path = version_dir.join(stripped); + storage::move_file(entry.path(), &final_path)?; + + tracing::info!("added override {}", final_path.display()); + } } - let mut file = File::create(&path)?; - std::io::copy(&mut bytes, &mut file)?; - - Ok(path) + Ok(version_dir) } } diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..151f58e --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,77 @@ +use std::{ + fs::File, + path::{Path, PathBuf}, + sync::LazyLock, +}; + +use color_eyre::eyre::{bail, eyre, OptionExt, Report, Result}; +use directories::ProjectDirs; +use sha2::{Digest, Sha512}; +use zip::ZipArchive; + +static DIRS: LazyLock = LazyLock::new(|| { + ProjectDirs::from("net", "uku3lig", "uklient") + .expect("could not instantiate project directories!") +}); + +pub fn make_directories() -> std::io::Result<()> { + std::fs::create_dir_all(DIRS.data_dir())?; + std::fs::create_dir_all(DIRS.cache_dir())?; + + Ok(()) +} + +pub async fn download_to_cache(url: &str, filename: &str, sha512: &str) -> Result { + let path = DIRS.cache_dir().join(filename); + + let response = crate::HTTP_CLIENT.get(url).send().await?; + let mut bytes = &*response.bytes().await?; // deref to get &[u8] + + let expected_hash = hex::decode(sha512)?; + let computed_hash = { + let mut sha512 = Sha512::new(); + sha512.update(bytes); + sha512.finalize() + }; + + if expected_hash[..] != computed_hash[..] { + bail!( + "hash mismatch! expected: {}, got {}", + hex::encode(expected_hash), + hex::encode(computed_hash), + ) + } + + let mut file = File::create(&path)?; + std::io::copy(&mut bytes, &mut file)?; + + Ok(path) +} + +pub fn decompress_zip(path: &Path) -> Result { + let dir = path.parent().ok_or_eyre("could not find parent")?; + let name = path.file_stem().ok_or_eyre("filename does not exist")?; + let extract_dir = dir.join(name); + + let file = File::open(path)?; + let mut archive = ZipArchive::new(file)?; + archive.extract(&extract_dir)?; + + Ok(extract_dir) +} + +pub fn get_version_directory(version: &str) -> Result { + let path = DIRS.data_dir().join("versions").join(version); + std::fs::create_dir_all(&path)?; + Ok(path) +} + +pub fn move_file, D: AsRef>(source: S, dest: D) -> Result<()> { + let parent = dest + .as_ref() + .parent() + .ok_or_else(|| eyre!("path {} does not have a parent", dest.as_ref().display()))?; + + std::fs::create_dir_all(parent)?; + std::fs::rename(source, dest).map_err(Report::from) +}