From 1ed3682df972a837e92806319c58ac7d180aa739 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Wed, 19 Oct 2022 19:28:09 -0300 Subject: [PATCH 01/34] wip --- Cargo.lock | 122 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/commands/gen_srf.rs | 29 ++++++++++ src/commands/mod.rs | 3 +- src/commands/sync.rs | 9 ++- src/main.rs | 8 ++- src/pbo.rs | 54 ++++++++---------- src/repository.rs | 7 +-- src/srf.rs | 108 +++++++++++++++++++---------------- 9 files changed, 249 insertions(+), 93 deletions(-) create mode 100644 src/commands/gen_srf.rs diff --git a/Cargo.lock b/Cargo.lock index 1a39bbe..b9f41d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,51 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + [[package]] name = "digest" version = "0.9.0" @@ -130,6 +175,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "flate2" version = "1.0.22" @@ -272,6 +323,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -289,11 +349,23 @@ dependencies = [ "byteorder", "clap", "md-5", + "rayon", "relative-path", "serde", "serde_json", "snafu", "ureq", + "walkdir", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -365,6 +437,30 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "relative-path" version = "1.6.1" @@ -407,6 +503,21 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sct" version = "0.7.0" @@ -599,6 +710,17 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasm-bindgen" version = "0.2.78" diff --git a/Cargo.toml b/Cargo.toml index a943c13..394acd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,6 @@ byteorder = "1" ureq = { version = "2", features = ["tls", "json"] } relative-path = { version = "1", features = ["serde"] } clap = { version = "3", features = ["derive"] } +rayon = "1" +walkdir = "2" # tinyvec = { version = "1.5", features = ["alloc", "rustc_1_55"] } diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs new file mode 100644 index 0000000..93e1d8c --- /dev/null +++ b/src/commands/gen_srf.rs @@ -0,0 +1,29 @@ +use crate::srf; +use rayon::prelude::*; +use std::fs::File; +use std::io::BufWriter; +use std::path::Path; +use walkdir::WalkDir; + +pub fn gen_srf(base_path: &Path) { + let paths: Vec<_> = WalkDir::new(base_path) + .max_depth(1) + .into_iter() + .filter_map(|e| e.ok()) + .map(|entry| entry.path().to_owned()) + .collect(); + + let mods: Vec<_> = paths + .par_iter() + .map(|path| { + let generated_srf = srf::scan_mod(&path).unwrap(); + + let path = path.join("mod.srf"); + + let writer = BufWriter::new(File::create(path).unwrap()); + serde_json::to_writer_pretty(writer, &generated_srf).unwrap(); + + generated_srf + }) + .collect(); +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index eac0ca6..dd933fd 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,2 @@ - +pub mod gen_srf; pub mod sync; - diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 3b671f8..a1e2a80 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -1,9 +1,9 @@ +use crate::{repository, srf}; +use snafu::{ResultExt, Whatever}; use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, Read}; use std::path::Path; -use snafu::{ResultExt, Whatever}; -use crate::{srf, repository}; fn diff_repos<'a>( local_repo: &repository::Repository, @@ -140,7 +140,7 @@ fn execute_command_list( commands: &[DownloadCommand], ) { for (i, command) in commands.iter().enumerate() { - println!("downloading {} of {}", i, commands.len()); + println!("downloading {} of {} - {}", i, commands.len(), command.file); let file_path = local_base.join(Path::new(&command.file)); std::fs::create_dir_all(file_path.parent().unwrap()).unwrap(); @@ -159,7 +159,7 @@ pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) { repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)).unwrap(); let local_repo: repository::Repository = { - let file = std::fs::File::open(base_path.join("./repo.json")); + let file = File::open(base_path.join("./repo.json")); match file { Err(e) => { @@ -187,4 +187,3 @@ pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) { execute_command_list(agent, repo_url, base_path, &download_commands); } - diff --git a/src/main.rs b/src/main.rs index 9aeb806..afbe567 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,11 +5,10 @@ use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; +mod commands; mod pbo; mod repository; mod srf; -mod commands; - #[derive(Subcommand)] enum Commands { @@ -20,6 +19,10 @@ enum Commands { #[clap(short, long)] local_path: PathBuf, }, + GenSrf { + #[clap(short, long)] + path: PathBuf, + }, } #[derive(Parser)] @@ -40,5 +43,6 @@ fn main() { repo_url, local_path, } => commands::sync::sync(&mut agent, &repo_url, &local_path), + Commands::GenSrf { path } => commands::gen_srf::gen_srf(&path), } } diff --git a/src/pbo.rs b/src/pbo.rs index db1066c..9116f71 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -5,12 +5,11 @@ use std::{ }; use byteorder::{LittleEndian, ReadBytesExt}; -use snafu::{ResultExt, Whatever}; +use snafu::{ResultExt, Snafu, Whatever}; #[derive(Debug)] pub struct Pbo { pub input: I, - pub header: PboEntry, pub header_len: u64, pub extensions: HashMap, pub entries: Vec, @@ -34,12 +33,16 @@ pub struct PboEntry { pub data_size: u32, } -fn read_string(input: &mut I) -> Result { +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("io error: {}", source))] + Io { source: std::io::Error }, +} + +fn read_string(input: &mut I) -> Result { let mut buf = Vec::new(); - input - .read_until(b'\0', &mut buf) - .with_whatever_context(|_| "read failure")?; + input.read_until(b'\0', &mut buf).context(IoSnafu {})?; let str = unsafe { CStr::from_bytes_with_nul_unchecked(&buf) }.to_string_lossy(); @@ -47,12 +50,10 @@ fn read_string(input: &mut I) -> Result { } impl PboEntry { - fn read(input: &mut I) -> Result { + fn read(input: &mut I) -> Result { let filename = read_string(input)?; - let r#type = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; + let r#type = input.read_u32::().context(IoSnafu {})?; let r#type = match r#type { 0x56657273 => EntryType::Vers, @@ -62,18 +63,10 @@ impl PboEntry { _ => panic!(), }; - let original_size = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; - let offset = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; - let timestamp = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; - let data_size = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; + let original_size = input.read_u32::().context(IoSnafu {})?; + let offset = input.read_u32::().context(IoSnafu {})?; + let timestamp = input.read_u32::().context(IoSnafu {})?; + let data_size = input.read_u32::().context(IoSnafu {})?; Ok(PboEntry { filename, @@ -86,7 +79,7 @@ impl PboEntry { } } -fn read_extensions(input: &mut I) -> Result, Whatever> { +fn read_extensions(input: &mut I) -> Result, Error> { let mut output_map = HashMap::new(); loop { @@ -103,14 +96,8 @@ fn read_extensions(input: &mut I) -> Result Pbo { - pub fn read(mut input: I) -> Result { - let header = PboEntry::read(&mut input)?; - - if !header.filename.is_empty() || header.r#type != EntryType::Vers { - panic!(); - } - - let extensions = read_extensions(&mut input)?; + pub fn read(mut input: I) -> Result { + let mut extensions = HashMap::new(); let mut entries = Vec::new(); @@ -121,6 +108,10 @@ impl Pbo { break; } + if entry.r#type == EntryType::Vers { + extensions = read_extensions(&mut input)?; + } + entries.push(entry); } @@ -128,7 +119,6 @@ impl Pbo { Ok(Pbo { input, - header, header_len, extensions, entries, diff --git a/src/repository.rs b/src/repository.rs index 4bd28bc..836460a 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -78,14 +78,11 @@ pub fn replicate_remote_repo_info(remote: &Repository) -> Repository { } } -pub fn get_repository_info<'a>( - agent: &'a mut ureq::Agent, - url: &'a str, -) -> Result> { +pub fn get_repository_info<'a>(agent: &'a mut ureq::Agent, url: &'a str) -> Result> { agent .get(url) .call() .context(HttpSnafu { url })? .into_json() - .context(DeserializationSnafu {}) + .context(DeserializationSnafu) } diff --git a/src/srf.rs b/src/srf.rs index ceb2dcb..6ad337b 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -1,12 +1,15 @@ use md5::{Digest, Md5}; +use rayon::prelude::*; use relative_path::RelativePathBuf; use serde::{Deserialize, Deserializer, Serialize}; -use snafu::{OptionExt, ResultExt, Whatever}; +use snafu::{OptionExt, ResultExt, Snafu, Whatever}; use std::io::{BufReader, Seek, SeekFrom}; use std::{ + io, io::{BufRead, Read}, path::Path, }; +use walkdir::WalkDir; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] @@ -25,6 +28,14 @@ pub enum FileType { Pbo, } +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("io error: {}", source))] + Io { source: io::Error }, + #[snafu(display("pbo error: {}", source))] + Pbo { source: crate::pbo::Error }, +} + impl FileType { fn from_legacy_srf(legacy_type: &str) -> Self { match legacy_type { @@ -73,28 +84,27 @@ impl Mod { } } -fn generate_hash(file: &mut BufReader, len: u64) -> Result { +fn generate_hash(file: &mut BufReader, len: u64) -> Result { let mut hasher = Md5::new(); let mut stream = file.take(len); - std::io::copy(&mut stream, &mut hasher).with_whatever_context(|_| "hashing failure")?; + std::io::copy(&mut stream, &mut hasher).context(IoSnafu {})?; let hash = hasher.finalize(); Ok(format!("{:X}", hash)) } -pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { - let mut file = BufReader::new( - std::fs::File::open(&path).with_whatever_context(|_| "failed to open file")?, - ); +pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { + let mut file = BufReader::new(std::fs::File::open(&path).context(IoSnafu {})?); let mut parts = Vec::new(); - let pbo = crate::pbo::Pbo::read(&mut file)?; + dbg!("scan pbo {}", path); + let pbo = crate::pbo::Pbo::read(&mut file).context(PboSnafu {})?; let mut offset = 0; - let length = pbo.input.seek(SeekFrom::End(0)).unwrap(); - pbo.input.seek(SeekFrom::Start(0)).unwrap(); + let length = pbo.input.seek(SeekFrom::End(0)).context(IoSnafu {})?; + pbo.input.seek(SeekFrom::Start(0)).context(IoSnafu {})?; { let header_hash = generate_hash(pbo.input, pbo.header_len)?; @@ -156,14 +166,11 @@ pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { }) } -pub fn scan_file(path: &Path, base_path: &Path) -> Result { - let file = std::fs::File::open(&path).with_whatever_context(|_| "failed to open file")?; +pub fn scan_file(path: &Path, base_path: &Path) -> Result { + let file = std::fs::File::open(&path).context(IoSnafu {})?; let mut parts = Vec::new(); - let file_len = file - .metadata() - .with_whatever_context(|_| "failed to acquire file metadata")? - .len(); + let file_len = file.metadata().context(IoSnafu {})?.len(); let mut reader = std::io::BufReader::new(file); let mut pos = 0; @@ -173,8 +180,7 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { let mut stream = reader.by_ref().take(5000000); let pre_copy_pos = pos; - let copied = std::io::copy(&mut stream, &mut hasher) - .with_whatever_context(|_| "failed to io copy into hasher")?; + let copied = std::io::copy(&mut stream, &mut hasher).context(IoSnafu {})?; pos += copied; let hash = hasher.finalize(); @@ -214,40 +220,44 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { }) } -fn recurse(path: &Path, base_path: &Path) -> Result, Whatever> { +fn recurse(path: &Path, base_path: &Path) -> Result, Error> { println!("recursing into {:#?}", &path); - let entries = path - .read_dir() - .with_whatever_context(|_| "failed to read directory entries")?; - - let mut files = Vec::new(); - for entry in entries { - let entry = entry.with_whatever_context(|_| "failed to read directory entry")?; - - let metadata = entry - .metadata() - .with_whatever_context(|_| "failed to read direntry metadata")?; - let path = entry.path(); - - if metadata.is_dir() { - files.append(&mut recurse(&path, base_path)?); - continue; - } - - let extension = path.extension(); - - match extension { - Some(extension) if extension == "pbo" => files.push(scan_pbo(&path, base_path)?), - _ => files.push(scan_file(&path, base_path)?), - } - } + let entries: Vec<_> = WalkDir::new(path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| { + // someday this spaghetti can just be replaced by Option::contains + if let Some(is_dir) = e + .metadata() + .ok() + .and_then(|metadata| Some(metadata.is_dir())) + { + !is_dir + } else { + false + } + }) + .map(|entry| entry.path().to_owned()) + .collect(); + + let files: Result, _> = entries + .par_iter() + .map(|path| { + let extension = path.extension(); + + match extension { + Some(extension) if extension == "pbo" => scan_pbo(&path, base_path), + _ => scan_file(&path, base_path), + } + }) + .collect(); - Ok(files) + Ok(files?) } // FIXME: ditch whatever errors -pub fn scan_mod(path: &Path) -> Result { +pub fn scan_mod(path: &Path) -> Result { let mut files = recurse(path, path)?; files.sort_by(|a, b| { @@ -403,7 +413,9 @@ fn read_legacy_srf_file( pub fn deserialize_legacy_srf(input: &mut I) -> Result { // swifty's legacy srf format is stateful - input.seek(SeekFrom::Start(0)).with_whatever_context(|_| "failed to rewind file")?; + input + .seek(SeekFrom::Start(0)) + .with_whatever_context(|_| "failed to rewind file")?; let mut files = Vec::::new(); let mut iter = input.lines().map(|line| line.expect("input.lines failed")); @@ -429,6 +441,7 @@ mod tests { use super::*; use std::io::Cursor; + /* #[test] fn legacy_srf_test() { let input = include_bytes!("mod.srf"); @@ -436,4 +449,5 @@ mod tests { let deserialized = deserialize_legacy_srf(&mut cursor).unwrap(); dbg!(deserialized); } + */ } From 605def7e76028a4bcaf8a3cc00eabd8c083d480d Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Fri, 11 Nov 2022 23:45:24 -0300 Subject: [PATCH 02/34] Skip the first PBO entry when generating the pbo checksum Previously we didn't consider the first Vers entry as a proper entry, matching Swifty behavior. --- src/commands/gen_srf.rs | 1 + src/srf.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index 93e1d8c..1562476 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -9,6 +9,7 @@ pub fn gen_srf(base_path: &Path) { let paths: Vec<_> = WalkDir::new(base_path) .max_depth(1) .into_iter() + .skip(1) .filter_map(|e| e.ok()) .map(|entry| entry.path().to_owned()) .collect(); diff --git a/src/srf.rs b/src/srf.rs index 6ad337b..137eb5c 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -119,7 +119,7 @@ pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { } // swifty, as always, does very strange things - for entry in &pbo.entries { + for entry in pbo.entries.iter().skip(1) { let hash = generate_hash(pbo.input, entry.data_size as u64)?; parts.push(Part { @@ -172,7 +172,7 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { let file_len = file.metadata().context(IoSnafu {})?.len(); - let mut reader = std::io::BufReader::new(file); + let mut reader = BufReader::new(file); let mut pos = 0; while pos < file_len { From 3d62bee4576cca93bb25e715114a269ed2b574b8 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 14 Nov 2022 01:52:51 -0300 Subject: [PATCH 03/34] Remove the download speed issue from the README It was an issue with my connection. Things can be improved by using threading / async but that's beyond the current scope --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 122b01f..392fd0c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,4 @@ In order of priority: * RelativePath and RelativePathBuf should be used in most cases. * We still need to convert Windows backslashes to a sane separator on *nix platforms * Use rayon for srf generation - * Investigate why download speeds are kinda wonky - * Seems to be an issue with my connection to the repo I was using for testing? * Properly deal with invalid PBOs \ No newline at end of file From 8ce3d136f0e15d94442ddc22d688015b13a40018 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 02:08:34 -0300 Subject: [PATCH 04/34] Cleanup error handling Still needs some improvement but we've ditched Whatever errors and a bunch of unwraps --- README.md | 3 +- src/commands/gen_srf.rs | 4 +- src/commands/sync.rs | 99 +++++++++++++++++++++---------- src/main.rs | 13 ++-- src/pbo.rs | 2 +- src/repository.rs | 11 +++- src/srf.rs | 128 +++++++++++++++++++++++++--------------- 7 files changed, 167 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 392fd0c..b2dc8fa 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,10 @@ In order of priority: * Create symlinks instead of copying mod files ## Big bucket list of things to do: - * Switch to proper errors instead of `snafu::Whatever` - * There's also way too much unwrapping because I got lazy * Implement part level downloading, instead of redownloading entire files * Clean up Path vs PathBuf vs &str vs String * RelativePath and RelativePathBuf should be used in most cases. * We still need to convert Windows backslashes to a sane separator on *nix platforms * Use rayon for srf generation + * Somewhat done but needs improvement * Properly deal with invalid PBOs \ No newline at end of file diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index 1562476..f3345aa 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -14,10 +14,10 @@ pub fn gen_srf(base_path: &Path) { .map(|entry| entry.path().to_owned()) .collect(); - let mods: Vec<_> = paths + let _mods: Vec<_> = paths .par_iter() .map(|path| { - let generated_srf = srf::scan_mod(&path).unwrap(); + let generated_srf = srf::scan_mod(path).unwrap(); let path = path.join("mod.srf"); diff --git a/src/commands/sync.rs b/src/commands/sync.rs index a1e2a80..c4b1c28 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -1,8 +1,8 @@ use crate::{repository, srf}; -use snafu::{ResultExt, Whatever}; +use snafu::{ResultExt, Snafu}; use std::collections::HashMap; use std::fs::File; -use std::io::{BufReader, Read}; +use std::io::{BufReader, Cursor, Read}; use std::path::Path; fn diff_repos<'a>( @@ -39,31 +39,53 @@ struct DownloadCommand { end: u64, } +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("io error: {}", source))] + Io { source: std::io::Error }, + #[snafu(display("Error while requesting repository data: {}", source))] + Http { + url: String, + + #[snafu(source(from(ureq::Error, Box::new)))] + source: Box, + }, + #[snafu(display("Failed to fetch repository info: {}", source))] + RepositoryFetch { source: repository::Error }, + #[snafu(display("SRF deserialization failure: {}", source))] + SrfDeserialization { source: serde_json::Error }, + #[snafu(display("Legacy SRF deserialization failure: {}", source))] + LegacySrfDeserialization { source: srf::Error }, + #[snafu(display("Failed to generate SRF: {}", source))] + SrfGeneration { source: srf::Error }, +} + fn diff_mod( agent: &ureq::Agent, repo_base_path: &str, local_base_path: &Path, remote_mod: &repository::Mod, -) -> Result, Whatever> { +) -> Result, Error> { // HACK HACK: this REALLY should be parsed through streaming rather than through buffering the whole thing + let remote_srf_url = format!("{}{}/mod.srf", repo_base_path, remote_mod.mod_name); let mut remote_srf = agent - .get(&format!( - "{}{}/mod.srf", - repo_base_path, remote_mod.mod_name - )) + .get(&remote_srf_url) .call() - .unwrap() + .context(HttpSnafu { + url: remote_srf_url, + })? .into_reader(); let mut buf = String::new(); - let _len = remote_srf.read_to_string(&mut buf).unwrap(); + let _len = remote_srf.read_to_string(&mut buf).context(IoSnafu)?; // yeet utf-8 bom, which is bad, not very useful and not supported by serde - let bomless = buf.trim_start_matches("\u{feff}"); + let bomless = buf.trim_start_matches('\u{feff}'); - let remote_srf: srf::Mod = serde_json::from_str(&bomless).unwrap(); /*.or_else(|_| { - srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(remote_srf))) - }).with_whatever_context(|_| "failed to deserialize remote srf")?;*/ + let remote_srf: srf::Mod = serde_json::from_str(bomless) + .context(SrfDeserializationSnafu) + .or_else(|_| srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(bomless)))) + .context(LegacySrfDeserializationSnafu)?; let local_path = local_base_path.join(Path::new(&format!("{}/", remote_mod.mod_name))); let srf_path = local_path.join(Path::new("mod.srf")); @@ -80,12 +102,12 @@ fn diff_mod( serde_json::from_reader(&mut reader) .or_else(|_| srf::deserialize_legacy_srf(&mut reader)) - .with_whatever_context(|_| "failed to deserialize local srf")? + .context(LegacySrfDeserializationSnafu)? } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - srf::scan_mod(&local_path).unwrap() + srf::scan_mod(&local_path).context(SrfGenerationSnafu)? } - _ => panic!(), + Err(e) => return Err(Error::Io { source: e }), } } }; @@ -116,14 +138,14 @@ fn diff_mod( // TODO: implement file diffing. for now, just download everything download_list.push(DownloadCommand { - file: format!("{}/{}", remote_srf.name, path.to_string().to_string()), + file: format!("{}/{}", remote_srf.name, path), begin: 0, end: file.length, }) } } else { download_list.push(DownloadCommand { - file: format!("{}/{}", remote_srf.name, path.to_string().to_string()), + file: format!("{}/{}", remote_srf.name, path), begin: 0, end: file.length, }) @@ -138,40 +160,51 @@ fn execute_command_list( remote_base: &str, local_base: &Path, commands: &[DownloadCommand], -) { +) -> Result<(), Error> { for (i, command) in commands.iter().enumerate() { println!("downloading {} of {} - {}", i, commands.len(), command.file); let file_path = local_base.join(Path::new(&command.file)); - std::fs::create_dir_all(file_path.parent().unwrap()).unwrap(); - let mut local_file = File::create(&file_path).unwrap(); + std::fs::create_dir_all(file_path.parent().expect("file_path did not have a parent")) + .context(IoSnafu)?; + let mut local_file = File::create(&file_path).context(IoSnafu)?; let remote_url = format!("{}{}", remote_base, command.file); - let mut reader = agent.get(&remote_url).call().unwrap().into_reader(); + let mut reader = agent + .get(&remote_url) + .call() + .context(HttpSnafu { + url: remote_url.clone(), + })? + .into_reader(); - std::io::copy(&mut reader, &mut local_file).unwrap(); + std::io::copy(&mut reader, &mut local_file).context(IoSnafu)?; } + + Ok(()) } -pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) { - let remote_repo = - repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)).unwrap(); +pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) -> Result<(), Error> { + let remote_repo = repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)) + .context(RepositoryFetchSnafu)?; - let local_repo: repository::Repository = { + let local_repo = { let file = File::open(base_path.join("./repo.json")); match file { Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { - repository::replicate_remote_repo_info(&remote_repo) + Ok(repository::replicate_remote_repo_info(&remote_repo)) } else { - panic!(); + return Err(Error::Io { source: e }); } } - Ok(file) => serde_json::from_reader(BufReader::new(file)).unwrap(), + Ok(file) => { + serde_json::from_reader(BufReader::new(file)).context(SrfDeserializationSnafu) + } } - }; + }?; let check = diff_repos(&local_repo, &remote_repo); @@ -185,5 +218,7 @@ pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) { println!("download commands: {:#?}", download_commands); - execute_command_list(agent, repo_url, base_path, &download_commands); + execute_command_list(agent, repo_url, base_path, &download_commands)?; + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index afbe567..b9bb2c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,4 @@ -use snafu::{ResultExt, Whatever}; -use std::fs::File; -use std::io::{BufReader, Read}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use clap::{Parser, Subcommand}; @@ -42,7 +39,11 @@ fn main() { Commands::Sync { repo_url, local_path, - } => commands::sync::sync(&mut agent, &repo_url, &local_path), - Commands::GenSrf { path } => commands::gen_srf::gen_srf(&path), + } => { + commands::sync::sync(&mut agent, &repo_url, &local_path).unwrap(); + } + Commands::GenSrf { path } => { + commands::gen_srf::gen_srf(&path); + } } } diff --git a/src/pbo.rs b/src/pbo.rs index 9116f71..feb0050 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -5,7 +5,7 @@ use std::{ }; use byteorder::{LittleEndian, ReadBytesExt}; -use snafu::{ResultExt, Snafu, Whatever}; +use snafu::{ResultExt, Snafu}; #[derive(Debug)] pub struct Pbo { diff --git a/src/repository.rs b/src/repository.rs index 836460a..a03950a 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -3,9 +3,14 @@ use snafu::prelude::*; use std::{fmt::Display, net::IpAddr, str::FromStr}; #[derive(Debug, Snafu)] -pub enum Error<'a> { +pub enum Error { #[snafu(display("Error while requesting repository data: {}", source))] - Http { url: &'a str, source: ureq::Error }, + Http { + url: String, + + #[snafu(source(from(ureq::Error, Box::new)))] + source: Box, + }, #[snafu(display("Error while deserializing: {}", source))] Deserialization { source: std::io::Error }, } @@ -78,7 +83,7 @@ pub fn replicate_remote_repo_info(remote: &Repository) -> Repository { } } -pub fn get_repository_info<'a>(agent: &'a mut ureq::Agent, url: &'a str) -> Result> { +pub fn get_repository_info(agent: &mut ureq::Agent, url: &str) -> Result { agent .get(url) .call() diff --git a/src/srf.rs b/src/srf.rs index 137eb5c..e8ac0ea 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -2,7 +2,7 @@ use md5::{Digest, Md5}; use rayon::prelude::*; use relative_path::RelativePathBuf; use serde::{Deserialize, Deserializer, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu, Whatever}; +use snafu::{OptionExt, ResultExt, Snafu}; use std::io::{BufReader, Seek, SeekFrom}; use std::{ io, @@ -34,14 +34,20 @@ pub enum Error { Io { source: io::Error }, #[snafu(display("pbo error: {}", source))] Pbo { source: crate::pbo::Error }, + #[snafu(display("legacy srf parse failure: {}", description))] + LegacySrfParseFailure { description: &'static str }, + #[snafu(display("legacy srf failed to parse size as u32: {}", source))] + LegacySrfU32ParseFailure { source: std::num::ParseIntError }, } impl FileType { - fn from_legacy_srf(legacy_type: &str) -> Self { + fn from_legacy_srf(legacy_type: &str) -> Result { match legacy_type { - "PBO" => Self::Pbo, - "FILE" => Self::File, - _ => panic!("unknown legacy file type"), + "PBO" => Ok(Self::Pbo), + "FILE" => Ok(Self::File), + _ => Err(Error::LegacySrfParseFailure { + description: "unknown legacy file type", + }), } } } @@ -96,15 +102,14 @@ fn generate_hash(file: &mut BufReader, len: u64) -> Result Result { - let mut file = BufReader::new(std::fs::File::open(&path).context(IoSnafu {})?); + let mut file = BufReader::new(std::fs::File::open(path).context(IoSnafu)?); let mut parts = Vec::new(); - dbg!("scan pbo {}", path); - let pbo = crate::pbo::Pbo::read(&mut file).context(PboSnafu {})?; + let pbo = crate::pbo::Pbo::read(&mut file).context(PboSnafu)?; let mut offset = 0; - let length = pbo.input.seek(SeekFrom::End(0)).context(IoSnafu {})?; - pbo.input.seek(SeekFrom::Start(0)).context(IoSnafu {})?; + let length = pbo.input.seek(SeekFrom::End(0)).context(IoSnafu)?; + pbo.input.seek(SeekFrom::Start(0)).context(IoSnafu)?; { let header_hash = generate_hash(pbo.input, pbo.header_len)?; @@ -167,10 +172,10 @@ pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { } pub fn scan_file(path: &Path, base_path: &Path) -> Result { - let file = std::fs::File::open(&path).context(IoSnafu {})?; + let file = std::fs::File::open(path).context(IoSnafu)?; let mut parts = Vec::new(); - let file_len = file.metadata().context(IoSnafu {})?.len(); + let file_len = file.metadata().context(IoSnafu)?.len(); let mut reader = BufReader::new(file); let mut pos = 0; @@ -231,7 +236,7 @@ fn recurse(path: &Path, base_path: &Path) -> Result, Error> { if let Some(is_dir) = e .metadata() .ok() - .and_then(|metadata| Some(metadata.is_dir())) + .map(|metadata| metadata.is_dir()) { !is_dir } else { @@ -247,16 +252,15 @@ fn recurse(path: &Path, base_path: &Path) -> Result, Error> { let extension = path.extension(); match extension { - Some(extension) if extension == "pbo" => scan_pbo(&path, base_path), - _ => scan_file(&path, base_path), + Some(extension) if extension == "pbo" => scan_pbo(path, base_path), + _ => scan_file(path, base_path), } }) .collect(); - Ok(files?) + files } -// FIXME: ditch whatever errors pub fn scan_mod(path: &Path) -> Result { let mut files = recurse(path, path)?; @@ -272,7 +276,7 @@ pub fn scan_mod(path: &Path) -> Result { for file in &files { hasher.update(&file.checksum); - hasher.update(file.path.to_string().to_lowercase().replace("\\", "/")); + hasher.update(file.path.to_string().to_lowercase().replace('\\', "/")); } format!("{:X}", hasher.finalize()) @@ -291,12 +295,14 @@ pub fn scan_mod(path: &Path) -> Result { }) } -fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Whatever> { +fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Error> { let mut split = line.split(':'); let r#type = split .next() - .with_whatever_context(|| "no first element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing type", + })? .to_string(); if r#type != "ADDON" { @@ -305,17 +311,24 @@ fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Whatever> { let name = split .next() - .with_whatever_context(|| "no second element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing name", + })? .to_string(); let size = split .next() - .with_whatever_context(|| "no third element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing size", + })? .parse() - .with_whatever_context(|_| "failed to parse size")?; + .context(LegacySrfU32ParseFailureSnafu)?; + let checksum = split .next() - .with_whatever_context(|| "no fourth element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing checksum", + })? .to_string(); Ok(( @@ -328,29 +341,37 @@ fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Whatever> { )) } -fn read_legacy_srf_part(line: &str) -> Result { +fn read_legacy_srf_part(line: &str) -> Result { let mut split = line.split(':'); let path = split .next() - .with_whatever_context(|| "no first element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing path", + })? .to_string(); let start: u64 = split .next() - .with_whatever_context(|| "no second element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing start", + })? .parse() - .with_whatever_context(|_| "start was not a u64")?; + .context(LegacySrfU32ParseFailureSnafu)?; let length: u64 = split .next() - .with_whatever_context(|| "no third element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing length", + })? .parse() - .with_whatever_context(|_| "start was not a u64")?; + .context(LegacySrfU32ParseFailureSnafu)?; let checksum = split .next() - .with_whatever_context(|| "no fourth element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing checksum", + })? .to_string(); Ok(Part { @@ -364,40 +385,51 @@ fn read_legacy_srf_part(line: &str) -> Result { fn read_legacy_srf_file( line: &str, lines: &mut impl Iterator, -) -> Result { +) -> Result { let mut split = line.split(':'); - let r#type = - FileType::from_legacy_srf(split.next().with_whatever_context(|| "no first element")?); + let r#type = FileType::from_legacy_srf(split.next().context(LegacySrfParseFailureSnafu { + description: "no first element", + })?)?; let path = RelativePathBuf::from( split .next() - .with_whatever_context(|| "no second element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing path", + })? .to_string(), ); let length: u64 = split .next() - .with_whatever_context(|| "no third element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing length", + })? .parse() - .with_whatever_context(|_| "length was not a u64")?; + .context(LegacySrfU32ParseFailureSnafu)?; let part_count: u32 = split .next() - .with_whatever_context(|| "no fourth element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing part count", + })? .parse() - .with_whatever_context(|_| "file_count was not a u32")?; + .context(LegacySrfU32ParseFailureSnafu)?; let checksum = split .next() - .with_whatever_context(|| "no fifth element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing checksum", + })? .to_string(); let mut parts = Vec::new(); for _ in 0..part_count { - let line = lines.next().with_whatever_context(|| "missing line")?; + let line = lines.next().context(LegacySrfParseFailureSnafu { + description: "part line missing", + })?; parts.push(read_legacy_srf_part(&line)?); } @@ -411,22 +443,24 @@ fn read_legacy_srf_file( }) } -pub fn deserialize_legacy_srf(input: &mut I) -> Result { +pub fn deserialize_legacy_srf(input: &mut I) -> Result { // swifty's legacy srf format is stateful - input - .seek(SeekFrom::Start(0)) - .with_whatever_context(|_| "failed to rewind file")?; + input.seek(SeekFrom::Start(0)).context(IoSnafu)?; let mut files = Vec::::new(); let mut iter = input.lines().map(|line| line.expect("input.lines failed")); - let first_line = iter.next().with_whatever_context(|| "no first line")?; + let first_line = iter.next().context(LegacySrfParseFailureSnafu { + description: "no first line", + })?; let (addon, file_count) = read_legacy_srf_addon(&first_line)?; for _ in 0..file_count { let file = read_legacy_srf_file( - &iter.next().with_whatever_context(|| "missing lines")?, + &iter.next().context(LegacySrfParseFailureSnafu { + description: "line missing", + })?, &mut iter, )?; From bf20a424f5f2c288b0c103af8c0d47f14412e231 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 14 Nov 2022 02:14:15 -0300 Subject: [PATCH 05/34] cargo update --- Cargo.lock | 340 ++++++++++++++++++++++------------------------------- Cargo.toml | 4 +- 2 files changed, 144 insertions(+), 200 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9f41d1..c06a8ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,15 +21,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bitflags" @@ -39,18 +39,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byteorder" @@ -60,9 +60,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" [[package]] name = "cfg-if" @@ -78,48 +78,55 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clap" -version = "3.1.3" +version = "4.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f8c0e2a6b902acc18214e24a6935cdaf8a8e34231913d4404dcaee659f65a1" +checksum = "0eb41c13df48950b20eb4cd0eefa618819469df1bffc49d11e8487c4ba0037e5" dependencies = [ "atty", "bitflags", "clap_derive", - "indexmap", - "lazy_static", - "os_str_bytes", + "clap_lex", + "once_cell", "strsim", "termcolor", - "textwrap", ] [[package]] name = "clap_derive" -version = "3.1.2" +version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d42c94ce7c2252681b5fed4d3627cc807b13dfc033246bd05d5b252399000e" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro-error", "proc-macro2", "quote", "syn", ] +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "crc32fast" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -127,9 +134,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -138,35 +145,44 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.8" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "lazy_static", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" dependencies = [ "cfg-if", - "lazy_static", ] [[package]] -name = "digest" -version = "0.9.0" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -177,57 +193,39 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "flate2" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if", "crc32fast", - "libc", "miniz_oxide", ] [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", ] -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -245,84 +243,53 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" -dependencies = [ - "autocfg", - "hashbrown", -] - [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.112" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "md-5" -version = "0.9.1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "block-buffer", "digest", - "opaque-debug", ] -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - [[package]] name = "memoffset" version = "0.6.5" @@ -334,12 +301,11 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", - "autocfg", ] [[package]] @@ -360,9 +326,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -370,30 +336,21 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "os_str_bytes" -version = "6.0.0" +version = "6.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] +checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "proc-macro-error" @@ -421,18 +378,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.30" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -463,9 +420,9 @@ dependencies = [ [[package]] name = "relative-path" -version = "1.6.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49a831dc1e13c9392b660b162333d4cb0033bbbdfe6a1687177e59e89037c86" +checksum = "0df32d82cedd1499386877b062ebe8721f806de80b08d183c70184ef17dd1d42" dependencies = [ "serde", ] @@ -487,9 +444,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" dependencies = [ "log", "ring", @@ -499,9 +456,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" @@ -530,18 +487,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -550,9 +507,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ "itoa", "ryu", @@ -561,9 +518,9 @@ dependencies = [ [[package]] name = "snafu" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eba135d2c579aa65364522eb78590cdf703176ef71ad4c32b00f58f7afb2df5" +checksum = "a152ba99b054b22972ee794cf04e5ef572da1229e33b65f3c57abbff0525a454" dependencies = [ "doc-comment", "snafu-derive", @@ -571,11 +528,11 @@ dependencies = [ [[package]] name = "snafu-derive" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7fe9b0669ef117c5cabc5549638528f36771f058ff977d7689deb517833a75" +checksum = "d5e79cdebbabaebb06a9bdbaedc7f159b410461f63611d4d0e3fb0fab8fed850" dependencies = [ - "heck 0.3.3", + "heck", "proc-macro2", "quote", "syn", @@ -595,35 +552,29 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.80" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" - [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -636,37 +587,31 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "typenum" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - [[package]] name = "untrusted" version = "0.7.1" @@ -675,9 +620,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" +checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" dependencies = [ "base64", "chunked_transfer", @@ -694,21 +639,20 @@ dependencies = [ [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" @@ -723,9 +667,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -733,13 +677,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -748,9 +692,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -758,9 +702,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -771,15 +715,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -797,9 +741,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.2" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" dependencies = [ "webpki", ] diff --git a/Cargo.toml b/Cargo.toml index 394acd4..ae9c746 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,11 @@ license = "GPL-3.0-or-later" serde = { version = "1", features = ["derive"] } serde_json = "1" snafu = "0.7" -md-5 = { version = "0.9", features = [] } +md-5 = { version = "0.10", features = [] } byteorder = "1" ureq = { version = "2", features = ["tls", "json"] } relative-path = { version = "1", features = ["serde"] } -clap = { version = "3", features = ["derive"] } +clap = { version = "4", features = ["derive"] } rayon = "1" walkdir = "2" # tinyvec = { version = "1.5", features = ["alloc", "rustc_1_55"] } From 95c7b0d2bfb08cd1a52a9cd55fbc125e3fc5f975 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 14 Nov 2022 02:14:35 -0300 Subject: [PATCH 06/34] sync: add dry run option --- src/commands/sync.rs | 6 ++++-- src/main.rs | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/commands/sync.rs b/src/commands/sync.rs index c4b1c28..1b8819a 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -185,7 +185,7 @@ fn execute_command_list( Ok(()) } -pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) -> Result<(), Error> { +pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path, dry_run: bool) -> Result<(), Error> { let remote_repo = repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)) .context(RepositoryFetchSnafu)?; @@ -218,7 +218,9 @@ pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) -> Result println!("download commands: {:#?}", download_commands); - execute_command_list(agent, repo_url, base_path, &download_commands)?; + if !dry_run { + execute_command_list(agent, repo_url, base_path, &download_commands)?; + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index b9bb2c2..659d936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,9 @@ enum Commands { #[clap(short, long)] local_path: PathBuf, + + #[clap(short, long)] + dry_run: bool, }, GenSrf { #[clap(short, long)] @@ -39,8 +42,9 @@ fn main() { Commands::Sync { repo_url, local_path, + dry_run, } => { - commands::sync::sync(&mut agent, &repo_url, &local_path).unwrap(); + commands::sync::sync(&mut agent, &repo_url, &local_path, dry_run).unwrap(); } Commands::GenSrf { path } => { commands::gen_srf::gen_srf(&path); From edfcba86d57f3022ecb34b65c819db845162dbb8 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 14 Nov 2022 02:25:55 -0300 Subject: [PATCH 07/34] gen-srf: remove an unecessary intermediate collect --- src/commands/gen_srf.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index f3345aa..d41cedc 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -6,17 +6,14 @@ use std::path::Path; use walkdir::WalkDir; pub fn gen_srf(base_path: &Path) { - let paths: Vec<_> = WalkDir::new(base_path) + let _mods: Vec<_> = WalkDir::new(base_path) .max_depth(1) .into_iter() .skip(1) + .par_bridge() .filter_map(|e| e.ok()) - .map(|entry| entry.path().to_owned()) - .collect(); - - let _mods: Vec<_> = paths - .par_iter() - .map(|path| { + .map(|entry| { + let path = entry.path(); let generated_srf = srf::scan_mod(path).unwrap(); let path = path.join("mod.srf"); From 32622dc3f61a4d6d74f1a46521575435839d7d10 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Sun, 20 Nov 2022 23:32:16 -0300 Subject: [PATCH 08/34] srf: fix checksum generation Swifty uses .NET's InvariantCulture IgnoreCase string comparison. I don't know of an easy way to exactly reimplement it in Rust, but for certain edge cases (i.e files which begin with numbers) folding to uppercase works. This will probably break again in the future. --- src/srf.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/srf.rs b/src/srf.rs index e8ac0ea..2204b28 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -266,9 +266,9 @@ pub fn scan_mod(path: &Path) -> Result { files.sort_by(|a, b| { a.path - .to_string() - .to_lowercase() - .cmp(&b.path.to_string().to_lowercase()) + .as_str() + .to_uppercase() + .cmp(&b.path.as_str().to_uppercase()) }); let checksum = { From d83c397cc62cbdbb99a276591edd55adbd7831c7 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 01:09:19 -0300 Subject: [PATCH 09/34] md5_digest: Introduce Small helper to deserialize MD5 digests to an array of u8. Simplifies handling the lifetimes of some HashSets we will be building in later commits --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + src/main.rs | 1 + src/md5_digest.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 src/md5_digest.rs diff --git a/Cargo.lock b/Cargo.lock index c06a8ae..b624a3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "idna" version = "0.3.0" @@ -314,6 +320,7 @@ version = "0.1.0" dependencies = [ "byteorder", "clap", + "hex", "md-5", "rayon", "relative-path", diff --git a/Cargo.toml b/Cargo.toml index ae9c746..c608cc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ relative-path = { version = "1", features = ["serde"] } clap = { version = "4", features = ["derive"] } rayon = "1" walkdir = "2" +hex = "0.4" # tinyvec = { version = "1.5", features = ["alloc", "rustc_1_55"] } diff --git a/src/main.rs b/src/main.rs index 659d936..993cdd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod commands; mod pbo; mod repository; mod srf; +mod md5_digest; #[derive(Subcommand)] enum Commands { diff --git a/src/md5_digest.rs b/src/md5_digest.rs new file mode 100644 index 0000000..f20dc24 --- /dev/null +++ b/src/md5_digest.rs @@ -0,0 +1,58 @@ +use std::fmt::{Debug, Formatter}; +use serde::{Serialize, Deserialize, Serializer, Deserializer}; + +#[derive(Hash, PartialEq, Eq, Clone)] +pub struct Md5Digest { + inner: [u8; 16], +} + +impl Md5Digest { + pub fn new(digest: &str) -> Self { + let mut inner = [0; 16]; + hex::decode_to_slice(digest, &mut inner); + Self { + inner, + } + } + + pub fn from_bytes(bytes: [u8; 16]) -> Self { + Self { + inner: bytes, + } + } +} + +impl Default for Md5Digest { + fn default() -> Self { + Self { + inner: [0; 16], + } + } +} + +impl Serialize for Md5Digest { + fn serialize(&self, serializer: S) -> Result where S: Serializer { + let digest = hex::encode_upper(&self.inner); + + serializer.serialize_str(&digest) + } +} + +impl<'de> Deserialize<'de> for Md5Digest { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + let digest = String::deserialize(deserializer)?; + + let mut inner = [0; 16]; + hex::decode_to_slice(digest, &mut inner).map_err(serde::de::Error::custom)?; + + Ok(Self::from_bytes(inner)) + } +} + +impl Debug for Md5Digest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Md5Digest") + .field("inner", &hex::encode_upper(&self.inner)) + .finish() + } +} \ No newline at end of file From d4b3612940f455a5c5afd7498187abaff9d56acd Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 01:10:01 -0300 Subject: [PATCH 10/34] mod_cache: Introduce --- src/main.rs | 1 + src/mod_cache.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/mod_cache.rs diff --git a/src/main.rs b/src/main.rs index 993cdd4..165972d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; mod commands; +mod mod_cache; mod pbo; mod repository; mod srf; diff --git a/src/mod_cache.rs b/src/mod_cache.rs new file mode 100644 index 0000000..4debafe --- /dev/null +++ b/src/mod_cache.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use crate::md5_digest::Md5Digest; + +#[derive(Serialize, Deserialize)] +pub struct ModCache { + version: u32, + pub mods: HashSet, +} + +impl ModCache { + pub fn new(mods: HashSet) -> Self { + Self { version: 1, mods } + } + + pub fn new_empty() -> Self { + Self { + version: 1, + mods: HashSet::new(), + } + } + + pub fn update_mod_checksum(&mut self, old_checksum: &Md5Digest, new_checksum: Md5Digest) { + self.mods.remove(old_checksum); + self.mods.insert(new_checksum); + } +} From df14cf27ac300cc002b0259df6443f7cb15d4a63 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 01:23:03 -0300 Subject: [PATCH 11/34] sync: add a progress bar and slightly improve the reliability of downloading Previously if a download failed we'd leave a partially downloaded file behind, which frequently crashed us when we parsed it later. Change it so we first download into a temp file and then copy to the location it's supposed to be. This might help with the implementation of delta downloads later, idk --- Cargo.lock | 159 +++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 + src/commands/sync.rs | 43 ++++++++---- 3 files changed, 171 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b624a3b..b6111b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,9 +60,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -78,9 +78,9 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clap" -version = "4.0.23" +version = "4.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb41c13df48950b20eb4cd0eefa618819469df1bffc49d11e8487c4ba0037e5" +checksum = "2148adefda54e14492fb9bddcc600b4344c5d1a3123bd666dcb939c6f0e0e57e" dependencies = [ "atty", "bitflags", @@ -113,6 +113,20 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -145,9 +159,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +checksum = "96bf8df95e795db1a4aca2957ad884a2df35413b24bbeb3114422f3cc21498e8" dependencies = [ "autocfg", "cfg-if", @@ -158,9 +172,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "422f23e724af1240ec469ea1e834d87a4b59ce2efe2c6a96256b0c47e2fd86aa" dependencies = [ "cfg-if", ] @@ -177,9 +191,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -197,6 +211,21 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.24" @@ -257,6 +286,27 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indicatif" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4295cbb7573c16d310e99e713cf9e75101eb190ab31fccd35f2d2691b4352b19" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.4" @@ -272,6 +322,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.137" @@ -298,9 +354,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] @@ -321,12 +377,14 @@ dependencies = [ "byteorder", "clap", "hex", + "indicatif", "md-5", "rayon", "relative-path", "serde", "serde_json", "snafu", + "tempfile", "ureq", "walkdir", ] @@ -341,6 +399,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.16.0" @@ -349,9 +413,9 @@ checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "os_str_bytes" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "percent-encoding" @@ -359,6 +423,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "portable-atomic" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15eb2c6e362923af47e13c23ca5afb859e83d54452c55b0b9ac763b8f7c1ac16" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -403,11 +473,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" dependencies = [ - "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -415,9 +484,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -425,6 +494,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + [[package]] name = "relative-path" version = "1.7.2" @@ -434,6 +512,15 @@ dependencies = [ "serde", ] +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -514,9 +601,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" dependencies = [ "itoa", "ryu", @@ -568,6 +655,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -577,6 +678,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -619,6 +730,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index c608cc8..5b5e61b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,7 @@ relative-path = { version = "1", features = ["serde"] } clap = { version = "4", features = ["derive"] } rayon = "1" walkdir = "2" +indicatif = "0.17" +tempfile = "3" hex = "0.4" # tinyvec = { version = "1.5", features = ["alloc", "rustc_1_55"] } diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 1b8819a..56ed04e 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -1,9 +1,11 @@ use crate::{repository, srf}; use snafu::{ResultExt, Snafu}; +use indicatif::{ProgressBar, ProgressState, ProgressStyle}; use std::collections::HashMap; use std::fs::File; -use std::io::{BufReader, Cursor, Read}; +use std::io::{BufReader, Cursor, Read, Seek, SeekFrom}; use std::path::Path; +use tempfile::tempfile; fn diff_repos<'a>( local_repo: &repository::Repository, @@ -164,22 +166,39 @@ fn execute_command_list( for (i, command) in commands.iter().enumerate() { println!("downloading {} of {} - {}", i, commands.len(), command.file); + // download into temp file first in case we have a failure. this avoids us writing garbage data + // which will later make us crash in gen_srf + let mut temp_download_file = tempfile().context(IoSnafu)?; + + let remote_url = format!("{}{}", remote_base, command.file); + + let mut response = agent.get(&remote_url).call().context(HttpSnafu { + url: remote_url.clone(), + })?; + + let mut pb = response + .header("Content-Length") + .and_then(|len| len.parse().ok()) + .map(ProgressBar::new) + .unwrap_or_else(ProgressBar::new_spinner); + + pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) + .progress_chars("#>-")); + + let mut reader = response.into_reader(); + + std::io::copy(&mut pb.wrap_read(reader), &mut temp_download_file).context(IoSnafu)?; + + // copy from temp to permanent file let file_path = local_base.join(Path::new(&command.file)); std::fs::create_dir_all(file_path.parent().expect("file_path did not have a parent")) .context(IoSnafu)?; let mut local_file = File::create(&file_path).context(IoSnafu)?; - let remote_url = format!("{}{}", remote_base, command.file); - - let mut reader = agent - .get(&remote_url) - .call() - .context(HttpSnafu { - url: remote_url.clone(), - })? - .into_reader(); - - std::io::copy(&mut reader, &mut local_file).context(IoSnafu)?; + temp_download_file.seek(SeekFrom::Start(0)); + std::io::copy(&mut temp_download_file, &mut local_file).context(IoSnafu)?; } Ok(()) From 9c43159d1273826bff2a7a3c52ffdb91802b7dcd Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 01:57:09 -0300 Subject: [PATCH 12/34] gen_srf, sync: cache repo-wide mod checksums. This allows us to use the checksums in repo.json, the first file we download, to skip mods with matching checksums, allowing us to not download mods' mod.srf --- src/commands/gen_srf.rs | 37 ++++++++++++----- src/commands/sync.rs | 92 ++++++++++++++++++++++++----------------- src/main.rs | 3 +- src/pbo.rs | 4 +- src/repository.rs | 3 +- src/srf.rs | 23 ++++++----- 6 files changed, 100 insertions(+), 62 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index d41cedc..4cdbaf8 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -1,27 +1,42 @@ +use crate::mod_cache::ModCache; +use crate::repository::Repository; use crate::srf; +use crate::srf::Mod; use rayon::prelude::*; -use std::fs::File; +use std::collections::HashSet; +use std::fs::{File, FileType}; use std::io::BufWriter; use std::path::Path; use walkdir::WalkDir; +use crate::md5_digest::Md5Digest; + +pub fn gen_srf_for_mod(mod_path: &Path) -> Md5Digest { + let generated_srf = srf::scan_mod(mod_path).unwrap(); + + let path = mod_path.join("mod.srf"); + + let writer = BufWriter::new(File::create(path).unwrap()); + serde_json::to_writer(writer, &generated_srf).unwrap(); + + generated_srf.checksum +} pub fn gen_srf(base_path: &Path) { - let _mods: Vec<_> = WalkDir::new(base_path) + let mods: HashSet = WalkDir::new(base_path) + .min_depth(1) .max_depth(1) .into_iter() - .skip(1) .par_bridge() .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_dir() && e.file_name().to_string_lossy().starts_with('@')) .map(|entry| { let path = entry.path(); - let generated_srf = srf::scan_mod(path).unwrap(); - - let path = path.join("mod.srf"); - - let writer = BufWriter::new(File::create(path).unwrap()); - serde_json::to_writer_pretty(writer, &generated_srf).unwrap(); - - generated_srf + gen_srf_for_mod(path) }) .collect(); + + let cache = ModCache::new(mods); + + let writer = BufWriter::new(File::create(base_path.join("nimble-cache.json")).unwrap()); + serde_json::to_writer(writer, &cache).unwrap(); } diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 56ed04e..f688997 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -1,39 +1,14 @@ +use crate::commands::gen_srf::gen_srf_for_mod; +use crate::mod_cache::ModCache; use crate::{repository, srf}; -use snafu::{ResultExt, Snafu}; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; +use snafu::{ResultExt, Snafu}; use std::collections::HashMap; use std::fs::File; -use std::io::{BufReader, Cursor, Read, Seek, SeekFrom}; +use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom}; use std::path::Path; use tempfile::tempfile; -fn diff_repos<'a>( - local_repo: &repository::Repository, - remote_repo: &'a repository::Repository, -) -> Vec<&'a repository::Mod> { - let mut downloads = Vec::new(); - - if local_repo.checksum == remote_repo.checksum { - return vec![]; - } - - let mut checksum_map = HashMap::new(); - - for _mod in &local_repo.required_mods { - checksum_map.insert(&_mod.checksum, _mod); - } - - for _mod in &remote_repo.required_mods { - match checksum_map.get(&_mod.checksum) { - None => downloads.push(_mod), - Some(local_mod) if local_mod.checksum != _mod.checksum => downloads.push(_mod), - _ => (), - } - } - - downloads -} - #[derive(Debug)] struct DownloadCommand { file: String, @@ -62,6 +37,24 @@ pub enum Error { SrfGeneration { source: srf::Error }, } +fn diff_repo<'a>( + mod_cache: &ModCache, + remote_repo: &'a repository::Repository, +) -> Vec<&'a repository::Mod> { + let mut downloads = Vec::new(); + + // repo checksums use the repo generation timestamp in the checksum calculation, so we can't really + // generate them for comparison. they aren't that useful anyway + + for _mod in &remote_repo.required_mods { + if !mod_cache.mods.contains(&_mod.checksum) { + downloads.push(_mod); + } + } + + downloads +} + fn diff_mod( agent: &ureq::Agent, repo_base_path: &str, @@ -96,7 +89,7 @@ fn diff_mod( if !local_path.exists() { srf::Mod::generate_invalid(&remote_srf) } else { - let file = File::open(&srf_path); + let file = File::open(srf_path); match file { Ok(file) => { @@ -204,17 +197,22 @@ fn execute_command_list( Ok(()) } -pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path, dry_run: bool) -> Result<(), Error> { +pub fn sync( + agent: &mut ureq::Agent, + repo_url: &str, + base_path: &Path, + dry_run: bool, +) -> Result<(), Error> { let remote_repo = repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)) .context(RepositoryFetchSnafu)?; - let local_repo = { - let file = File::open(base_path.join("./repo.json")); + let mut mod_cache = { + let file = File::open(base_path.join("nimble-cache.json")); match file { Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { - Ok(repository::replicate_remote_repo_info(&remote_repo)) + Ok(ModCache::new_empty()) } else { return Err(Error::Io { source: e }); } @@ -225,21 +223,39 @@ pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path, dry_run: } }?; - let check = diff_repos(&local_repo, &remote_repo); + let check = diff_repo(&mod_cache, &remote_repo); println!("mods to check: {:#?}", check); let mut download_commands = vec![]; - for _mod in check { + for _mod in &check { download_commands.extend(diff_mod(agent, repo_url, base_path, _mod).unwrap()); } println!("download commands: {:#?}", download_commands); - if !dry_run { - execute_command_list(agent, repo_url, base_path, &download_commands)?; + if dry_run { + return Ok(()); + } + + let res = execute_command_list(agent, repo_url, base_path, &download_commands); + + if let Err(e) = res { + println!("an error occured while downloading: {}", e); + println!("you should retry this command"); } + // gen_srf for the mods we downloaded + for _mod in &check { + let checksum = gen_srf_for_mod(&base_path.join(Path::new(&_mod.mod_name))); + + mod_cache.update_mod_checksum(&_mod.checksum, checksum); + } + + // reserialize the cache + let writer = BufWriter::new(File::create(base_path.join("nimble-cache.json")).unwrap()); + serde_json::to_writer(writer, &mod_cache).unwrap(); + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 165972d..524a5f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; +use crate::commands::gen_srf::gen_srf_for_mod; mod commands; mod mod_cache; @@ -24,7 +25,7 @@ enum Commands { GenSrf { #[clap(short, long)] path: PathBuf, - }, + } } #[derive(Parser)] diff --git a/src/pbo.rs b/src/pbo.rs index feb0050..da55901 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -37,6 +37,8 @@ pub struct PboEntry { pub enum Error { #[snafu(display("io error: {}", source))] Io { source: std::io::Error }, + #[snafu(display("unknown pbo type: {}", r#type))] + PboType { r#type: u32 }, } fn read_string(input: &mut I) -> Result { @@ -60,7 +62,7 @@ impl PboEntry { 0x43707273 => EntryType::Cprs, 0x456e6372 => EntryType::Enco, 0x00000000 => EntryType::None, - _ => panic!(), + _ => return Err(Error::PboType { r#type }), }; let original_size = input.read_u32::().context(IoSnafu {})?; diff --git a/src/repository.rs b/src/repository.rs index a03950a..6c13c1d 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Deserializer, Serialize}; use snafu::prelude::*; use std::{fmt::Display, net::IpAddr, str::FromStr}; +use crate::md5_digest::Md5Digest; #[derive(Debug, Snafu)] pub enum Error { @@ -39,7 +40,7 @@ where pub struct Mod { pub mod_name: String, #[serde(rename = "checkSum")] // why - pub checksum: String, + pub checksum: Md5Digest, pub enabled: bool, } diff --git a/src/srf.rs b/src/srf.rs index 2204b28..fe76650 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -3,6 +3,7 @@ use rayon::prelude::*; use relative_path::RelativePathBuf; use serde::{Deserialize, Deserializer, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; +use std::ffi::OsStr; use std::io::{BufReader, Seek, SeekFrom}; use std::{ io, @@ -10,6 +11,7 @@ use std::{ path::Path, }; use walkdir::WalkDir; +use crate::md5_digest::Md5Digest; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] @@ -76,14 +78,14 @@ pub struct File { #[serde(rename_all = "PascalCase")] pub struct Mod { pub name: String, - pub checksum: String, + pub checksum: Md5Digest, pub files: Vec, } impl Mod { pub fn generate_invalid(remote: &Self) -> Self { Self { - checksum: "INVALID".into(), + checksum: Default::default(), files: vec![], ..remote.clone() } @@ -230,14 +232,11 @@ fn recurse(path: &Path, base_path: &Path) -> Result, Error> { let entries: Vec<_> = WalkDir::new(path) .into_iter() + .filter_entry(|e| e.file_name() != OsStr::new("mod.srf")) .filter_map(|e| e.ok()) .filter(|e| { // someday this spaghetti can just be replaced by Option::contains - if let Some(is_dir) = e - .metadata() - .ok() - .map(|metadata| metadata.is_dir()) - { + if let Some(is_dir) = e.metadata().ok().map(|metadata| metadata.is_dir()) { !is_dir } else { false @@ -276,10 +275,12 @@ pub fn scan_mod(path: &Path) -> Result { for file in &files { hasher.update(&file.checksum); - hasher.update(file.path.to_string().to_lowercase().replace('\\', "/")); + let relpath = file.path.as_str().to_lowercase().replace('\\', "/"); + hasher.update(relpath); } - format!("{:X}", hasher.finalize()) + let output = hasher.finalize(); + Md5Digest::from_bytes(output.into()) }; Ok(Mod { @@ -324,13 +325,15 @@ fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Error> { .parse() .context(LegacySrfU32ParseFailureSnafu)?; - let checksum = split + let checksum_digest = split .next() .context(LegacySrfParseFailureSnafu { description: "addon line missing checksum", })? .to_string(); + let checksum = Md5Digest::new(&checksum_digest); + Ok(( Mod { name, From 37d8a6d68282e89fbe0ceba60039e4b7b5e71a99 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 02:32:49 -0300 Subject: [PATCH 13/34] project-wide: make Clippy happy yes this should've been split. --- src/commands/gen_srf.rs | 4 +--- src/commands/sync.rs | 8 ++++---- src/main.rs | 1 - src/md5_digest.rs | 27 ++++++++++++++------------- src/pbo.rs | 2 +- src/repository.rs | 9 --------- src/srf.rs | 4 +++- 7 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index 4cdbaf8..35b04de 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -1,10 +1,8 @@ use crate::mod_cache::ModCache; -use crate::repository::Repository; use crate::srf; -use crate::srf::Mod; use rayon::prelude::*; use std::collections::HashSet; -use std::fs::{File, FileType}; +use std::fs::File; use std::io::BufWriter; use std::path::Path; use walkdir::WalkDir; diff --git a/src/commands/sync.rs b/src/commands/sync.rs index f688997..c2f27a4 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -165,11 +165,11 @@ fn execute_command_list( let remote_url = format!("{}{}", remote_base, command.file); - let mut response = agent.get(&remote_url).call().context(HttpSnafu { + let response = agent.get(&remote_url).call().context(HttpSnafu { url: remote_url.clone(), })?; - let mut pb = response + let pb = response .header("Content-Length") .and_then(|len| len.parse().ok()) .map(ProgressBar::new) @@ -180,7 +180,7 @@ fn execute_command_list( .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) .progress_chars("#>-")); - let mut reader = response.into_reader(); + let reader = response.into_reader(); std::io::copy(&mut pb.wrap_read(reader), &mut temp_download_file).context(IoSnafu)?; @@ -190,7 +190,7 @@ fn execute_command_list( .context(IoSnafu)?; let mut local_file = File::create(&file_path).context(IoSnafu)?; - temp_download_file.seek(SeekFrom::Start(0)); + temp_download_file.seek(SeekFrom::Start(0)).context(IoSnafu)?; std::io::copy(&mut temp_download_file, &mut local_file).context(IoSnafu)?; } diff --git a/src/main.rs b/src/main.rs index 524a5f3..2a8353c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; -use crate::commands::gen_srf::gen_srf_for_mod; mod commands; mod mod_cache; diff --git a/src/md5_digest.rs b/src/md5_digest.rs index f20dc24..f444722 100644 --- a/src/md5_digest.rs +++ b/src/md5_digest.rs @@ -1,18 +1,27 @@ use std::fmt::{Debug, Formatter}; +use hex::FromHexError; use serde::{Serialize, Deserialize, Serializer, Deserializer}; +use snafu::{ResultExt, Snafu}; -#[derive(Hash, PartialEq, Eq, Clone)] +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("hex digest decode error: {}", source))] + HexDecode { source: FromHexError } +} + +#[derive(Default, Hash, PartialEq, Eq, Clone)] pub struct Md5Digest { inner: [u8; 16], } impl Md5Digest { - pub fn new(digest: &str) -> Self { + pub fn new(digest: &str) -> Result { let mut inner = [0; 16]; - hex::decode_to_slice(digest, &mut inner); - Self { + hex::decode_to_slice(digest, &mut inner).context(HexDecodeSnafu)?; + + Ok(Self { inner, - } + }) } pub fn from_bytes(bytes: [u8; 16]) -> Self { @@ -22,14 +31,6 @@ impl Md5Digest { } } -impl Default for Md5Digest { - fn default() -> Self { - Self { - inner: [0; 16], - } - } -} - impl Serialize for Md5Digest { fn serialize(&self, serializer: S) -> Result where S: Serializer { let digest = hex::encode_upper(&self.inner); diff --git a/src/pbo.rs b/src/pbo.rs index da55901..bb37be6 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -15,7 +15,7 @@ pub struct Pbo { pub entries: Vec, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum EntryType { Vers, Cprs, diff --git a/src/repository.rs b/src/repository.rs index 6c13c1d..ed82aa2 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -75,15 +75,6 @@ pub struct Repository { pub servers: Vec, } -pub fn replicate_remote_repo_info(remote: &Repository) -> Repository { - Repository { - required_mods: vec![], - optional_mods: vec![], - checksum: "INVALID".into(), - ..remote.clone() - } -} - pub fn get_repository_info(agent: &mut ureq::Agent, url: &str) -> Result { agent .get(url) diff --git a/src/srf.rs b/src/srf.rs index fe76650..cca066e 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -40,6 +40,8 @@ pub enum Error { LegacySrfParseFailure { description: &'static str }, #[snafu(display("legacy srf failed to parse size as u32: {}", source))] LegacySrfU32ParseFailure { source: std::num::ParseIntError }, + #[snafu(display("failed to decode md5 digest: {}", source))] + DigestParse { source: crate::md5_digest::Error }, } impl FileType { @@ -332,7 +334,7 @@ fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Error> { })? .to_string(); - let checksum = Md5Digest::new(&checksum_digest); + let checksum = Md5Digest::new(&checksum_digest).context(DigestParseSnafu)?; Ok(( Mod { From 77110524aff39115c7a543368b131d813bc50a45 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 03:05:53 -0300 Subject: [PATCH 14/34] ci: Introduce --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..23b84c9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: ci +on: + pull_request: + push: + branches: + - master +jobs: + test: + name: test + env: + RUST_BACKTRACE: 1 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose From f66257dae5b398ab77f511001731d43d115204a0 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 03:21:56 -0300 Subject: [PATCH 15/34] ci: Fix existing tests --- src/pbo.rs | 14 +- src/srf.rs | 8 +- test_files/ace_advanced_ballistics.pbo | Bin 0 -> 200888 bytes test_files/legacy_format_mod.srf | 404 +++++++++++++++++++++++++ 4 files changed, 413 insertions(+), 13 deletions(-) create mode 100644 test_files/ace_advanced_ballistics.pbo create mode 100644 test_files/legacy_format_mod.srf diff --git a/src/pbo.rs b/src/pbo.rs index bb37be6..29673d7 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -130,17 +130,13 @@ impl Pbo { #[cfg(test)] mod tests { + use std::io::Cursor; use super::*; #[test] - fn magic_check() { - dbg!(Pbo::read(&mut std::io::BufReader::new( - std::fs::File::open( - "/home/vitorhnn/arma_crap/mods/@ACE/addons/ace_advanced_ballistics.pbo", - ) - .unwrap(), - )) - .unwrap()); - //Pbo::read(&mut std::io::BufReader::new(std::fs::File::open("/home/vitorhnn/arma_crap/mods/@ACE/addons/ace_advanced_ballistics.pbo.ace_3.13.6.60-8bd4922f.bisign").unwrap())).unwrap(); + fn basic_pbo_test() { + let bytes = include_bytes!("../test_files/ace_advanced_ballistics.pbo"); + let pbo = Pbo::read(Cursor::new(&bytes)).unwrap(); + assert_eq!(pbo.entries.len(), 49); } } diff --git a/src/srf.rs b/src/srf.rs index cca066e..d35cd25 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -480,13 +480,13 @@ mod tests { use super::*; use std::io::Cursor; - /* #[test] fn legacy_srf_test() { - let input = include_bytes!("mod.srf"); + let input = include_bytes!("../test_files/legacy_format_mod.srf"); let mut cursor = Cursor::new(input); let deserialized = deserialize_legacy_srf(&mut cursor).unwrap(); - dbg!(deserialized); + + assert_eq!(deserialized.name, "@lambs_danger"); + assert_eq!(deserialized.checksum, Md5Digest::new("44C1B8021822F80E1E560689D2AAB0BF").unwrap()); } - */ } diff --git a/test_files/ace_advanced_ballistics.pbo b/test_files/ace_advanced_ballistics.pbo new file mode 100644 index 0000000000000000000000000000000000000000..759ccbc6c159618913fd5f2a8eb9876be1935eed GIT binary patch literal 200888 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 QTuFJ2{O#Q9+YYAv55{LZtN;K2 literal 0 HcmV?d00001 diff --git a/test_files/legacy_format_mod.srf b/test_files/legacy_format_mod.srf new file mode 100644 index 0000000..479f993 --- /dev/null +++ b/test_files/legacy_format_mod.srf @@ -0,0 +1,404 @@ +ADDON:@lambs_danger:19:44C1B8021822F80E1E560689D2AAB0BF +FILE:addons\lambs_formations.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:737EA58E2EE46B8239598668575EAFB0 +lambs_formations.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:71E2B570D4B0316E7858050739578DB7 +PBO:addons\lambs_formations.pbo:2819:6:220C39158BE1C18AB20687E0E03B1D58 +$$HEADER$$:0:216:BE7418C36416DCD00F882E27348FC1CB +CfgFSMs.hpp:216:987:CFDE4D162DB701BC4DCD5FC79A7BFD38 +CfgVehicles.hpp:1203:440:C99C6EE330AF584F99B94F7C859932B4 +config.bin:1643:747:D310780B5E1CC02CF2229553FC816ED2 +script_component.hpp:2390:408:6A5498A0B99942090A4A50B5D7F37ABA +$$END$$:2798:21:1FC9E69678E63C547ECDFC3BD67A8398 +FILE:addons\lambs_eventhandlers.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:F48D237CC6B2C99E0842ED2D5F1D8AB0 +lambs_eventhandlers.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:ECB2C30761439838F33DB3B4ADA42FA0 +FILE:readme.txt:957:1:02FEF8C4604C7189377ABFF90C1B47E4 +readme.txt_957:0:957:EA3A793730701C4F27FE39DCB88F4706 +FILE:readme.md:2387:1:2DF21248D5DB1DE3654C57E993A38FD6 +readme.md_2387:0:2387:658850005DA11A7043565BB6B0B1661B +FILE:mod.cpp:479:1:9DF16337588972D424E5DC119972BDAA +mod.cpp_479:0:479:46B36C9107AB26E0EAACCA84FB0A423A +FILE:meta.cpp:104:1:FF97B8075E3B8C773D44096EAF9A07C2 +meta.cpp_104:0:104:F0F08A7D2045FA96A496C2D1B0AAC2CD +FILE:license:19534:1:7B2591755967A7F41FDB0C542CFAE9FF +license_19534:0:19534:511B28AFA1D29E84D2E89C059ADD0D8B +FILE:lambs_logo.paa:31929:1:9C27128E68D2FE2D5EFB21A950EC52BF +lambs_logo.paa_31929:0:31929:D1B1E9C7753605181DB216C03A999528 +FILE:keys\lambs_danger_2.5.3.bikey:180:1:5198A9AC6BC619F64D55551AA73A1A2F +lambs_danger_2.5.3.bikey_180:0:180:BA9044BDBF484C52F9B662B2F961D7A0 +PBO:addons\lambs_eventhandlers.pbo:12198:15:91269D931987E7A9B853CB75A8EFBF21 +$$HEADER$$:0:594:76CE2F41B30974AAFB086FB9C3F733CB +CfgEventHandlers.hpp:594:599:237618E58F277CEBEF86825829F13870 +config.bin:1193:702:B31828C7496258D7E413BD4F9018DB8C +functions\fnc_explosionEH.sqf:1895:2111:F42042EDA0629CBA3E2080702EEFEB3A +functions\fnc_explosionEH.sqfc:4006:2701:30D8ECCB02C90041C0970B1A9B25E088 +functions\script_component.hpp:6707:63:F79D7AE49F671841B156C25B72AF15AF +script_component.hpp:6770:430:3C4ADD25AEC8ADB48962EFB9B769FA73 +settings.sqf:7200:648:303108E1A095639FF86661A795CF9B4D +stringtable.xml:7848:2340:C53769754347A5901C2831167EF60305 +XEH_preInit.sqf:10188:120:721B158055283E540726854A39D547FA +XEH_preInit.sqfc:10308:1232:F005399A8A9A7EE833856818ECB1F832 +XEH_PREP.hpp:11540:20:7F11106C12EA7F81AF0510904479C370 +XEH_preStart.sqf:11560:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:11618:559:0425F41C63171062DF8D1170E6FE1921 +$$END$$:12177:21:3E859B4848D6C40F0D2A2BD1AF38F8FA +FILE:addons\lambs_danger.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:5CF0BD7560284DA7F384929E14EE01B5 +lambs_danger.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:82A1F0DC78AD4F7E27DFD8B8A6EB4680 +PBO:addons\lambs_danger.pbo:330183:96:A1D0F4ECC96FD6CF0FBB7DC32A047E63 +$$HEADER$$:0:4877:9688762986C766A9A8B68936CA54EF50 +Cfg3DEN.hpp:4877:2845:FB02A50320CC954B8685E961DD49B729 +CfgEventHandlers.hpp:7722:457:19B8184236818F7BF8FC4E94ECFF8FD5 +CfgVehicles.hpp:8179:2597:73D0CB005FCF18CB1D14B2FF2E9476EC +config.bin:10776:5919:2FBF3787DF34068438FA736DDF1905D4 +functions\fnc_brain.sqf:16695:3771:3A9E17C16BB38A0EAD5850F00834BEDA +functions\fnc_brain.sqfc:20466:3372:EFC448DD590FBDFFBA053F9F93B290B3 +functions\fnc_brainAdjust.sqf:23838:412:946A22E915FEFD2954657487DD242A65 +functions\fnc_brainAdjust.sqfc:24250:560:D592D1B4163BD520C462528A0ED823A6 +functions\fnc_brainAssess.sqf:24810:1751:AB3F120DC27A4819B48B7A4D1BB00BC0 +functions\fnc_brainAssess.sqfc:26561:2247:51286F997D93C314008A96E8D980B5E4 +functions\fnc_brainEngage.sqf:28808:1710:70042BA127974CC0B785972C71745F33 +functions\fnc_brainEngage.sqfc:30518:2370:77B6E2B603232F55D0B87FA1A626D46A +functions\fnc_brainForced.sqf:32888:1466:5B7E71848A10E7CF666AC655102AA832 +functions\fnc_brainForced.sqfc:34354:2014:5B4D4A6E156E1EF442E61607DF554A83 +functions\fnc_brainHide.sqf:36368:2558:3072D954DF214FCC5E07840D0C805709 +functions\fnc_brainHide.sqfc:38926:2428:19C4C7C0158E7E4E6CE74354BBCB97AF +functions\fnc_brainReact.sqf:41354:1080:91E9704802337E172C9FCB6360102562 +functions\fnc_brainReact.sqfc:42434:1438:8CACC28FBE61D874A891F9D307C77900 +functions\fnc_brainVehicle.sqf:43872:6748:2CE95C45C83B343A1BB8615E297A75A2 +functions\fnc_brainVehicle.sqfc:50620:6619:87D48E29D216CDC85BB264A20099DC1E +functions\fnc_fsmAllowAnimation.sqf:57239:608:D90B2547C2EE441D591058AA8C5EFAA0 +functions\fnc_fsmAllowAnimation.sqfc:57847:1137:A845CF4E75CA65DDE01ABA228EEE91F3 +functions\fnc_isForced.sqf:58984:569:E53B7D5C5B4A58E659E79A93616C0BC7 +functions\fnc_isForced.sqfc:59553:1103:FECC511F584721A497E53BB138E6F691 +functions\fnc_isForcedExit.sqf:60656:412:8506F5CCCF703712B5C003F09476BF4D +functions\fnc_isForcedExit.sqfc:61068:798:3A154A5486E7175243A0B040DE7B8947 +functions\fnc_isLeader.sqf:61866:498:50779E28210548D6EE6A85E522F1F315 +functions\fnc_isLeader.sqfc:62364:983:BC523F2526B9B2D405D2CFF28F51FFA6 +functions\fnc_tactics.sqf:63347:1135:E985F8A752889ADA56144A0507629EA9 +functions\fnc_tactics.sqfc:64482:1160:4FD2693D83CD899E5D04961DB9C8203C +functions\fnc_tacticsAssault.sqf:65642:3756:DD75B27F76BD51592ABA2899002F6AFA +functions\fnc_tacticsAssault.sqfc:69398:4196:2F4FEAE88F5AC940BF5ADD4AF6B1B24A +functions\fnc_tacticsAssess.sqf:73594:8686:66A08ADDDBDA703B490D30B016E437F5 +functions\fnc_tacticsAssess.sqfc:82280:8151:6E8F51E0E3B95EE3732F20BD4F1CBC91 +functions\fnc_tacticsAttack.sqf:90431:2769:17F8C4AB6AFB2A852DD1A52F2133F246 +functions\fnc_tacticsAttack.sqfc:93200:3199:0828F1F397E3B29CFABCBE98A48C0B34 +functions\fnc_tacticsContact.sqf:96399:5004:471B285FEED25F5E5420420380AC5B90 +functions\fnc_tacticsContact.sqfc:101403:5003:BE65C94637F46F65F050D8B373691D1F +functions\fnc_tacticsCQB.sqf:106406:2428:5EE6C94F964216C5FF52E4DACB6CCC4C +functions\fnc_tacticsCQB.sqfc:108834:2758:45FBB4262079D9929DDDB73A33C5E02F +functions\fnc_tacticsFlank.sqf:111592:4658:D77851D1E238BE7CA42E354C40610BA0 +functions\fnc_tacticsFlank.sqfc:116250:5000:639EA5390C7F05384B524FC40C2EAA1C +functions\fnc_tacticsGarrison.sqf:121250:3752:DBF0ACB97C9D18CE9A56E9C05D09A80E +functions\fnc_tacticsGarrison.sqfc:125002:3949:89D3B0CE538FA93B00C45B0F943C14C7 +functions\fnc_tacticsHide.sqf:128951:4442:CFCA1A653A5B2936B9366933DA377052 +functions\fnc_tacticsHide.sqfc:133393:4813:E71C653767E774692641BAD3A9843764 +functions\fnc_tacticsHold.sqf:138206:3071:3105D7CAEFB156AD4920E4C8E5784188 +functions\fnc_tacticsHold.sqfc:141277:3532:EF81CD606C026AE3150EEDA945364A66 +functions\fnc_tacticsProfiles.sqf:144809:484:D302541CA34A064AD8914FD52D038576 +functions\fnc_tacticsProfiles.sqfc:145293:422:28DA7C8425CCF98D89E3250B7818FA8C +functions\fnc_tacticsReinforce.sqf:145715:4217:4AB16F23B62182522F9568E5DA422448 +functions\fnc_tacticsReinforce.sqfc:149932:4508:2BE299CD74BF6E52D90E813A6C554BBF +functions\fnc_tacticsSuppress.sqf:154440:3268:3C21D73CD268B5836726A010F4DA0E8B +functions\fnc_tacticsSuppress.sqfc:157708:3739:A3E232627316D2849877860A7B0410D4 +functions\script_component.hpp:161447:357:4160B1B37A93ECD21FF00001262FCCD3 +functions\ZEN\fnc_setDisableAI.sqf:161804:181:ECE155DD9CD00877D7955B6FD531EEB9 +functions\ZEN\fnc_setDisableAI.sqfc:161985:749:D40B1D1C0DF93E1819077A8F5AC7AA0B +functions\ZEN\fnc_setDisableGroupAI.sqf:162734:184:F9A74D127BA6B527B873D686AF3038CC +functions\ZEN\fnc_setDisableGroupAI.sqfc:162918:760:3F50E40B52D2304CBD26057EFCB38A25 +functions\ZEN\fnc_setHasRadio.sqf:163678:183:65D5D8980AAAE46372B9E0BF666DE73D +functions\ZEN\fnc_setHasRadio.sqfc:163861:749:C2889AC1BFBE4CD8983A536475716E30 +functions\ZEN\fnc_setReinforcement.sqf:164610:192:946065547BAAB7F96E470FB40BE2BF9A +functions\ZEN\fnc_setReinforcement.sqfc:164802:766:B18542DA7A8CE721CC9DEA807F23838E +functions\ZEN\fnc_showHasRadio.sqf:165568:194:19C74EE06EE6CEEA28239424E1DB0D65 +functions\ZEN\fnc_showHasRadio.sqfc:165762:796:2A1774F606B7EE00740A67257E3623DF +functions\ZEN\fnc_showReinforcement.sqf:166558:203:B6C5B264295CC59B35F7F6A9C634E832 +functions\ZEN\fnc_showReinforcement.sqfc:166761:817:8D31B801CB81F5C16BD1152AA2219748 +functions\ZEN\fnc_showSetDisableAI.sqf:167578:192:8988FA12B081A0717FD3C42DECD47B54 +functions\ZEN\fnc_showSetDisableAI.sqfc:167770:805:D273D30520954D4EF4C77F9F821DC764 +functions\ZEN\fnc_showSetDisableGroupAI.sqf:168575:195:ACF4AE12A2F8F739784B55962CDB60CD +functions\ZEN\fnc_showSetDisableGroupAI.sqfc:168770:809:A81D2B883778414AAFF0509CFA82D9EF +functions\ZEN\script_component.hpp:169579:56:D866B7162B3E59053882EDD1982858AC +functions\ZeusModules\fnc_moduleConfigureGroupAI.sqf:169635:1641:D557389BEAECF8A60F6A1B82C196D134 +functions\ZeusModules\fnc_moduleConfigureGroupAI.sqfc:171276:2319:D7663AF54A879B456E5E7A2396B3C973 +functions\ZeusModules\fnc_moduleDisableAI.sqf:173595:1367:58ED437E775F2F92E5A81549CAE19A02 +functions\ZeusModules\fnc_moduleDisableAI.sqfc:174962:2219:7A508C69451460004EE7B642483272D5 +functions\ZeusModules\fnc_moduleSetRadio.sqf:177181:1366:99935E475D6C3011E0372C77F3FD57C5 +functions\ZeusModules\fnc_moduleSetRadio.sqfc:178547:2222:6F905A86662D3DC839D42C08853BD06E +functions\ZeusModules\script_component.hpp:180769:56:D866B7162B3E59053882EDD1982858AC +script_component.hpp:180825:388:4C1299C1F16A25975D34BAFEDC88BDDA +scripts\lambs_danger.fsm:181213:36867:8F0A6A6A6596E013CBD20AA3CEC580A4 +scripts\lambs_dangerCivilian.fsm:218080:55323:440D26AE2FCCF3327127196E59DCE2F7 +settings.sqf:273403:4277:79225537134DDFCE65051672CC34EB11 +stringtable.xml:277680:26386:B14E244B044120D132E6E42D7C5C6168 +XEH_postInit.sqf:304066:980:6F419C8BBD9C45E8565F0F7A41AF2CA8 +XEH_postInit.sqfc:305046:1085:3E9EB95DF0CA796CC232C8B629C1EE7E +XEH_preInit.sqf:306131:2029:4B6DD4D5C5D4EB7AFD3014F1B08BF879 +XEH_preInit.sqfc:308160:6000:D47F47CDCD8511F2C198C82AACEE012F +XEH_preInitClient.sqf:314160:5674:BC0455DA96D03E975B791DE8FAA9C17E +XEH_preInitClient.sqfc:319834:4951:C2A7436EAADC1D49E810F243D18A3FD0 +XEH_PREP.hpp:324785:895:268350334D76F121F5569F3D7590BAFC +XEH_preStart.sqf:325680:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:325738:2392:0D909AEE25FF887EBFD0C10EA20C03F6 +ZEN_CfgContext.hpp:328130:2032:365581A19621888A7BFDDBFCF19EEC85 +$$END$$:330162:21:93378E0B3224E6BBC32D02CE6BA853D2 +FILE:addons\lambs_wp.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:4F93826E1262C4A7999C53844D1AD750 +lambs_wp.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:69CF0E69A89F9182D919A18918311F4D +PBO:addons\lambs_wp.pbo:426889:113:F132945DBD24A70ECC362CFEEF75D495 +$$HEADER$$:0:5619:0224664498A2303EC3B78734AE50CE51 +Cfg3DEN.hpp:5619:1978:CADCB90987799698F9B60BCAC95CB985 +CfgEventHandlers.hpp:7597:388:B509493BA0169AA3124D9197146A51BD +CfgVehicles.hpp:7985:1407:F5B3530F6CA3F454A34EDBC3255ED28B +CfgWaypoints.hpp:9392:3347:2AEBE2863527EAC45221A83C5AB97C6E +config.bin:12739:31992:35925542E897A91482CA7353B3C6D8E0 +functions\fnc_doArtillery.sqf:44731:6077:E478AF32801F0E64E5AAA3CEA75BD387 +functions\fnc_doArtillery.sqfc:50808:5461:1F4C2EFEE243149244F27D1BB9E395B1 +functions\fnc_doAssaultUnitReset.sqf:56269:1127:9E87C183C682BE2B8126614FDFD653D9 +functions\fnc_doAssaultUnitReset.sqfc:57396:1628:4B7B42E58BE6E915474274E1927369A4 +functions\fnc_sideHasArtillery.sqf:59024:769:F66C971929BB4BEFB95A6AC5B172683B +functions\fnc_sideHasArtillery.sqfc:59793:1263:0AA2B666D52562917DBB97E1896460FD +functions\fnc_taskArtillery.sqf:61056:783:2E720389D841D102676D615CBF3A556A +functions\fnc_taskArtillery.sqfc:61839:810:208FBD0B1FE7642C82FAAEFF84930389 +functions\fnc_taskArtilleryRegister.sqf:62649:1186:1FE9C681E10EEA097BD051D9C3D42DDB +functions\fnc_taskArtilleryRegister.sqfc:63835:1772:78E519DA5CA98B0DD771EF835E6B0EDC +functions\fnc_taskAssault.sqf:65607:6470:660BC148C72385F9810B56E8698F68BA +functions\fnc_taskAssault.sqfc:72077:5914:40ACA6DE7F5FA3C0BBD572FC34D8C6DA +functions\fnc_taskCamp.sqf:77991:8641:721EF05DA11DDE6E4069072E00D0A663 +functions\fnc_taskCamp.sqfc:86632:8592:41BFA0DC0F050EA66BB8C45D0C7DE29A +functions\fnc_taskCQB.sqf:95224:8063:922079F5C1D76154AB746A90D30D88BB +functions\fnc_taskCQB.sqfc:103287:7332:8A8691E7AA5F22B9DDEA5384F05DE5F4 +functions\fnc_taskCreep.sqf:110619:4292:4933B6BC2B0EBC928A795DD2BA555D4C +functions\fnc_taskCreep.sqfc:114911:4394:A1C4AC0894ABA8EF66053DD998EFE06A +functions\fnc_taskGarrison.sqf:119305:8304:107A8B1B993A7E22270B51D94EAD2C5F +functions\fnc_taskGarrison.sqfc:127609:8084:F0808EAB62D79DAB015BFBE939698536 +functions\fnc_taskHunt.sqf:135693:3724:63957CE21CAD1B977C5CDF77EADEF411 +functions\fnc_taskHunt.sqfc:139417:3738:AB8A45F11BB1D6F52AE1D9D09B022A41 +functions\fnc_taskPatrol.sqf:143155:4974:8C935FB5556CA516273A9552F76518AE +functions\fnc_taskPatrol.sqfc:148129:4872:15059228862F40B045E642D18E2E9869 +functions\fnc_taskReset.sqf:153001:2972:15B405F68055C1FE0AAE146B9A4140F2 +functions\fnc_taskReset.sqfc:155973:3135:7F6AC11954A1DDA51E321A19BD9B32FB +functions\fnc_taskRush.sqf:159108:3285:F8E9A12CFB3B7637AF311404A4361AD9 +functions\fnc_taskRush.sqfc:162393:3513:819F2D36255103F65377E828AE68FE74 +functions\Modules\fnc_moduleArtillery.sqf:165906:3311:9D6BFA7F54F9B668EB19AFDCAFC46277 +functions\Modules\fnc_moduleArtillery.sqfc:169217:3173:CD41FCF1719990511A3CAE572F589E75 +functions\Modules\fnc_moduleArtilleryRegister.sqf:172390:2546:F4A1C40C75C6567070C1B26ADCB71F41 +functions\Modules\fnc_moduleArtilleryRegister.sqfc:174936:3489:4B8F575F074B74743828723D5224696C +functions\Modules\fnc_moduleAssault.sqf:178425:6769:24F078A728D388AEBE405C760F0BB335 +functions\Modules\fnc_moduleAssault.sqfc:185194:5339:9D7306707BAB3213EE390E1B5BA512B7 +functions\Modules\fnc_moduleCamp.sqf:190533:5831:BAE7F7771CF5A658601147B78F1EC1C6 +functions\Modules\fnc_moduleCamp.sqfc:196364:4980:BED8106FBE956B973E4175FE239FE03A +functions\Modules\fnc_moduleCQB.sqf:201344:6041:AE3ED70C7C6826D1F6988065C039C4A4 +functions\Modules\fnc_moduleCQB.sqfc:207385:5345:EFB43DB0A77CBAED7AA521C968105D32 +functions\Modules\fnc_moduleCreep.sqf:212730:3688:0F7E2E604B2B210F608D2D63D23C42DF +functions\Modules\fnc_moduleCreep.sqfc:216418:3806:07CE6662771864CC73469F75F1CFE605 +functions\Modules\fnc_moduleGarrison.sqf:220224:6709:4D7AC8E118351EE82DB4BBBEE7370592 +functions\Modules\fnc_moduleGarrison.sqfc:226933:5244:9408EC1C0AD082003BF35F4E86C557C6 +functions\Modules\fnc_moduleHunt.sqf:232177:4489:360EA09A21C593DD8D2C9170FE874B16 +functions\Modules\fnc_moduleHunt.sqfc:236666:4266:2D039546E3A51915C7AB168906CBEEE3 +functions\Modules\fnc_modulePatrol.sqf:240932:6130:5D1F7821FB93ECCBB91B968775F7C390 +functions\Modules\fnc_modulePatrol.sqfc:247062:5045:3AC56D2079BA36863439B7B99F951E7E +functions\Modules\fnc_moduleReset.sqf:252107:1685:EEE43796E630DD9052E075646F1DB3F6 +functions\Modules\fnc_moduleReset.sqfc:253792:2544:1F41E2EA4AEE48EBB108DD1BD7733A27 +functions\Modules\fnc_moduleRush.sqf:256336:3707:E7B4A2E94B7D8E1D09C42BFD958DBE15 +functions\Modules\fnc_moduleRush.sqfc:260043:3838:71D79B528267E327AF052864E2E96AAB +functions\Modules\fnc_moduleTarget.sqf:263881:1306:A9F1AB6944FBEC9209B1AFC1575A1332 +functions\Modules\fnc_moduleTarget.sqfc:265187:2308:B9D69B4B72524C3EA410BD157AB2126A +functions\Modules\script_component.hpp:267495:52:BCFEB3A429EE725D21BBBF99534BEF4A +functions\script_component.hpp:267547:52:BCFEB3A429EE725D21BBBF99534BEF4A +functions\ZEN\fnc_setArtilleryRegister.sqf:267599:160:077ADFFA68FD4676CF19958C5EDE5DD2 +functions\ZEN\fnc_setArtilleryRegister.sqfc:267759:1149:85FFA1F58B5F725ECDEB7539E1BC6CE7 +functions\ZEN\fnc_setCamp.sqf:268908:230:3F7E4910BB232C6BB2D48B357C639B15 +functions\ZEN\fnc_setCamp.sqfc:269138:1212:78252EE564033B5013E0364EC5F23938 +functions\ZEN\fnc_setCQB.sqf:270350:222:B09C99DA3A20E79789336DDBB73A6511 +functions\ZEN\fnc_setCQB.sqfc:270572:1189:8FE1353BB5E1628FE215AE793A794F5B +functions\ZEN\fnc_setCreep.sqf:271761:183:F25FAAAB7000DB256A1CA242C8EF5614 +functions\ZEN\fnc_setCreep.sqfc:271944:1161:0828B42725A0DD5F21EF1984C3672F28 +functions\ZEN\fnc_setGarrison.sqf:273105:234:DFE08419BD900C84A02CC1B5BEA25619 +functions\ZEN\fnc_setGarrison.sqfc:273339:1219:071C5C64EEAFD85B1B3EBD99AAE3E0E0 +functions\ZEN\fnc_setHunt.sqf:274558:182:D0F7AB7D5F70EBD754409F1B26004757 +functions\ZEN\fnc_setHunt.sqfc:274740:1157:C73C8BFB688B6CF742B8F47E5E2DBE23 +functions\ZEN\fnc_setPatrol.sqf:275897:232:606694DDD29CFC2729C6B9AA23D0AFE0 +functions\ZEN\fnc_setPatrol.sqfc:276129:1214:B6DA6C9392DA8DAD7CEEA8E128A934F9 +functions\ZEN\fnc_setReset.sqf:277343:183:A8BED9C430FB1DCD357A72DCA3A48DEC +functions\ZEN\fnc_setReset.sqfc:277526:1160:3A73EE6262320534E4F8198640CB0C48 +functions\ZEN\fnc_setRush.sqf:278686:182:B54A75E680F1169AC235A7B6C85252A4 +functions\ZEN\fnc_setRush.sqfc:278868:1158:A552EE8CC1FECAD633A616FCA1B16F1A +functions\ZEN\fnc_setTarget.sqf:280026:1147:22B897EAA5A3DCFA6F34034AF4E43A1E +functions\ZEN\fnc_setTarget.sqfc:281173:2092:C05222A7FD408671C900DD91B038C98B +functions\ZEN\script_component.hpp:283265:385:508A3ACF17E36C1965D4805A32BB6A43 +modules.hpp:283650:23821:D30791E8FC728143064A42CF4F5ED79E +script_component.hpp:307471:1718:808193A6D0F40D20C6BA946543DB738E +scripts\fnc_wpAssault.sqf:309189:684:559EC322507056D98AA471967F583927 +scripts\fnc_wpAssault.sqfc:309873:955:D37B229ED96DBB53DA374788264C34F7 +scripts\fnc_wpCQB.sqf:310828:595:4079E74F884552642D954500F4F4A964 +scripts\fnc_wpCQB.sqfc:311423:930:03D3A96E70D04F0DA0C24FB4A10B02AA +scripts\fnc_wpCreep.sqf:312353:635:5A5D4DF463D820C6856CE1E283A6284B +scripts\fnc_wpCreep.sqfc:312988:996:ED9B713167BA202EB7E4BE7EADDB54BA +scripts\fnc_wpGarrison.sqf:313984:530:24FADA58C60C9F1695953A9226F695C3 +scripts\fnc_wpGarrison.sqfc:314514:889:04302F165ABD89BE3C91F3A47A8BEFA3 +scripts\fnc_wpHunt.sqf:315403:561:5E4A8607E5790024C2ED7BF166E876BB +scripts\fnc_wpHunt.sqfc:315964:902:F1191558D79CA47D2807A80BD4608EB6 +scripts\fnc_wpPatrol.sqf:316866:525:793B2A5A2F67A2885E376066103FEE47 +scripts\fnc_wpPatrol.sqfc:317391:878:DD3EAA144979A664379FC51B5BFE7AAB +scripts\fnc_wpRetreat.sqf:318269:681:79B8B455246BDFDA7EE8536C8763ECD7 +scripts\fnc_wpRetreat.sqfc:318950:1004:9E483A4CB4525931AFE48FC47BBBE30A +scripts\fnc_wpRush.sqf:319954:658:62195CCC01378C372D4D244975CB6FD1 +scripts\fnc_wpRush.sqfc:320612:1037:1E327B631B55E83A737666614E710AE0 +scripts\script_component.hpp:321649:52:BCFEB3A429EE725D21BBBF99534BEF4A +settings.sqf:321701:1029:CDC77462AF9B2EB0B7857E19AFBDFAC0 +stringtable.xml:322730:77881:166A637A88A924BA034EB255D0EE9F29 +XEH_postInit.sqf:400611:84:842CF923F9828E021028826AE0217070 +XEH_postInit.sqfc:400695:478:54884901134010D7A5F93C085D6FD840 +XEH_preInit.sqf:401173:3054:7564B853180AC5AC1B455E844F83D06C +XEH_preInit.sqfc:404227:6928:B81B65864D2918EF63656869442E28F0 +XEH_PREP.hpp:411155:935:071475B2B86A0FA5522AC2414B1E43B9 +XEH_preStart.sqf:412090:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:412148:2291:368A7DCAD157F069C531637171C20A8E +ZEN_CfgContext.hpp:414439:2978:D0CC9FE087A8860720DF5D7840EC89C4 +ZEN_CfgWaypointTypes.hpp:417417:1779:00819D5EF726DC19E03D9F73F10E8AE4 +zeusModules.hpp:419196:7672:40FB62A2CEEA0B8D28C3B9650ED27CC7 +$$END$$:426868:21:7C6728859852B43D71F0419C93D2FF72 +FILE:addons\lambs_range.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:4C70F16F5657AC703564A9F35524FB23 +lambs_range.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:5E4C344EAEE65BA6E997FFD20C0BA230 +PBO:addons\lambs_range.pbo:990:5:1E5583825F8B718B804DF72CACF43564 +$$HEADER$$:0:179:DFD124320264A6CC217C3C36E559F045 +CfgVehicles.hpp:179:100:E37CC5586921F842B3CD200B8D47C43A +config.bin:279:302:3CFBE4CA898ED67CEB1C7E75C5CBF14A +script_component.hpp:581:388:2D8B206DB8749D5DB4FF95447C769739 +$$END$$:969:21:F28F4590461D05514A4C2BB797E5C7FC +FILE:addons\lambs_main.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:58D683758460D7190FFB9846E7D8E543 +lambs_main.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:E6B1E4CD9835898694C1D49D8E0CB781 +PBO:addons\lambs_main.pbo:368123:136:0A0EB200A2A220C33C6F240A58D65E10 +$$HEADER$$:0:7568:D422F222D087A6F7E8C990675DFDC6E7 +CfgEventHandlers.hpp:7568:388:B509493BA0169AA3124D9197146A51BD +CfgFactionClasses.hpp:7956:493:91641BBB00E31D60DEAAB154A490B08D +config.bin:8449:1102:CF30AF2E9611DAB64D067C5958BE781D +functions\debug\fnc_debugDangerType.sqf:9551:1025:BF095511B0F80EE3B6FFAE74069437BF +functions\debug\fnc_debugDangerType.sqfc:10576:1386:4213D0DC5A2DA264634EB877291DE303 +functions\debug\fnc_debugDraw.sqf:11962:9774:B397638148B5D1371483BF9073813693 +functions\debug\fnc_debugDraw.sqfc:21736:9258:FD451A235B6589E750692274DF921D76 +functions\debug\fnc_debugLog.sqf:30994:393:E7A437911EE4A7FE8B78E3FDF2486947 +functions\debug\fnc_debugLog.sqfc:31387:759:320BB76FF4DAFBDF0496055F38BD8F34 +functions\debug\fnc_debugMarkerColor.sqf:32146:600:13297C4EFA81A6A0F53757DFB500BAB1 +functions\debug\fnc_debugMarkerColor.sqfc:32746:915:1D9434CD070B1CB63F906902251ACFD6 +functions\debug\fnc_debugObjectColor.sqf:33661:723:E5486A83B7B111C35B4DEC72363166D0 +functions\debug\fnc_debugObjectColor.sqfc:34384:941:CCDFBCCF5F36C588F5D81DA2589B0540 +functions\debug\fnc_dotMarker.sqf:35325:1026:F495F2A491D4B55B470FFB4C7EB8F31C +functions\debug\fnc_dotMarker.sqfc:36351:1261:C46ACA0F4FE13903914571BEA8870C51 +functions\debug\fnc_zoneMarker.sqf:37612:1374:57DB12693F53C5868C6FF7DCE448EFE7 +functions\debug\fnc_zoneMarker.sqfc:38986:1259:7420AB82CC3CB6183431C7046D5C40B0 +functions\debug\script_component.hpp:40245:54:6CCE9C47B1794B440210AFE451B227F3 +functions\fnc_addShareInformationHandler.sqf:40299:768:B9A7EF1547857E04AD60962C9910AFCE +functions\fnc_addShareInformationHandler.sqfc:41067:634:53CE2EE3494C5BA0A40ABC763C5163B9 +functions\fnc_doAnimation.sqf:41701:2715:DD95E8DB9B8BEB995CF825842206DB69 +functions\fnc_doAnimation.sqfc:44416:2963:03F8CE5784AF6D1F95F2CC1FD8DF8E07 +functions\fnc_doCallout.sqf:47379:4119:886247A542F088212AB88DC3A34CC582 +functions\fnc_doCallout.sqfc:51498:4740:9FB7D50746717021A050AE6F90E59DC4 +functions\fnc_doGesture.sqf:56238:881:146D76790AD0931B54FF99BF7FCF4DD7 +functions\fnc_doGesture.sqfc:57119:1129:5704974E4DB443CF1521E6A4D9B9E04D +functions\fnc_doShareInformation.sqf:58248:3604:7ADCB6295B454860BA1791D0DDA6C2FE +functions\fnc_doShareInformation.sqfc:61852:4262:23A2F50E2DD929DA5E5CCE90A61F1EA9 +functions\fnc_eventCallback.sqf:66114:407:9E8A4068EC37A2E2D88A784F4FA5CE09 +functions\fnc_eventCallback.sqfc:66521:570:D2DE8C7C341B4C4B42AED6EC7CBC5D93 +functions\fnc_findBuildings.sqf:67091:1568:C18D8DF7219B33F4780080F49F5046FE +functions\fnc_findBuildings.sqfc:68659:1852:0C3D906F791A6B1733D92418927AD8A4 +functions\fnc_findClosestTarget.sqf:70511:1415:5F9BF2C6A7BE81226CBFF6D5FC3359AE +functions\fnc_findClosestTarget.sqfc:71926:2283:003F6EA8EF9C0FE9C391A2981461F3DC +functions\fnc_findCover.sqf:74209:4254:E117AAE9FD8C4D57C37C64A9C46061BC +functions\fnc_findCover.sqfc:78463:4029:E9FD799B42F8027B4E4CD08D5EB48856 +functions\fnc_findNearbyFriendlies.sqf:82492:718:5E3A45180FC54C775AEF57B216188E1A +functions\fnc_findNearbyFriendlies.sqfc:83210:1071:7DE9CDE6B618F08AD4966FEB2218FEC7 +functions\fnc_findOverwatch.sqf:84281:2311:906B9C025573BFDEC74DFCEE1924BCD0 +functions\fnc_findOverwatch.sqfc:86592:2430:71DFB088585E96D3891EB08D2FFEB260 +functions\fnc_findReadyUnits.sqf:89022:1135:3EF7A8BDC1A5F92DC463992C375C3199 +functions\fnc_findReadyUnits.sqfc:90157:1750:C7B6BC24D8B56599F3F3A32FB486C67C +functions\fnc_findReadyVehicles.sqf:91907:794:ACFE0122120C9B1DA2FE9AA9115095A3 +functions\fnc_findReadyVehicles.sqfc:92701:1137:C568D8D60FCC92B05225A9BDA338BC2B +functions\fnc_getShareInformationParams.sqf:93838:1511:C1718F94693184C38497FB12D751955C +functions\fnc_getShareInformationParams.sqfc:95349:2171:F6016F5AF23234F248210CA6F4C93B90 +functions\fnc_initModules.sqf:97520:756:B514410FFDF9C851E84F4793FE95BADF +functions\fnc_initModules.sqfc:98276:1405:E800BEEBAD4CEADBC232E2EBE725FF86 +functions\fnc_isAlive.sqf:99681:330:D04893CE48C9F38ACA343B20E3D1E1CE +functions\fnc_isAlive.sqfc:100011:580:DE46A9838EBD1793123611B5B45A409D +functions\fnc_isIndoor.sqf:100591:515:A361941DFC17161D58A62C9B6DE51FF6 +functions\fnc_isIndoor.sqfc:101106:1033:068FF953D1F2573ADF39716020714474 +functions\fnc_isNight.sqf:102139:528:34914BCE2B8BBEA3B777E54269F51362 +functions\fnc_isNight.sqfc:102667:834:6165DB54B060972A277C33E43FD867AF +functions\fnc_parseData.sqf:103501:1599:8130D308EA072D54405314A60244FBEC +functions\fnc_parseData.sqfc:105100:2101:AAB1909D219DD6C363D4A0E227E839E6 +functions\fnc_removeEventhandlers.sqf:107201:358:B008188ECD6556EE54EF928C857CAA08 +functions\fnc_removeEventhandlers.sqfc:107559:631:99536077754FC331382E73E3441F3EEF +functions\fnc_shouldSuppressPosition.sqf:108190:1734:457C2197E33627461A31F1D880EB6542 +functions\fnc_shouldSuppressPosition.sqfc:109924:2045:BDF54333D899DD5A36B04DB953E71B2F +functions\fnc_showDialog.sqf:111969:17484:6D336E3F536E5D53927CF060B5330BD3 +functions\fnc_showDialog.sqfc:129453:14165:089DDDBDAA108C6639E3549888AC0AD7 +functions\GroupAction\fnc_doGroupAssault.sqf:143618:1310:34BAE09754E0794D4424CB45BD189E11 +functions\GroupAction\fnc_doGroupAssault.sqfc:144928:2054:2847BF4987D9BBE25E2BF39F335C644B +functions\GroupAction\fnc_doGroupFlank.sqf:146982:1709:60049778C18682541DFD487013CD0FCF +functions\GroupAction\fnc_doGroupFlank.sqfc:148691:2531:7D2409FCE2672C859835F115D09C0ABF +functions\GroupAction\fnc_doGroupHide.sqf:151222:1306:60260116385013714F7EC1FCF3C93443 +functions\GroupAction\fnc_doGroupHide.sqfc:152528:1817:DD9131CF8023A4005DDD6948EA04398D +functions\GroupAction\fnc_doGroupStaticDeploy.sqf:154345:5372:6018D726A77E9CDAFFE842F7A3A06D96 +functions\GroupAction\fnc_doGroupStaticDeploy.sqfc:159717:5490:C8504C70E3B9BFA9DFDADF48EC0F831E +functions\GroupAction\fnc_doGroupStaticFind.sqf:165207:1459:E39A5896C2C944B9C74549D3288D1434 +functions\GroupAction\fnc_doGroupStaticFind.sqfc:166666:2072:AA6A220115C32AA26A0F6BC8CBD6E9DE +functions\GroupAction\fnc_doGroupStaticPack.sqf:168738:4096:FCEE7BBE0224CB50753F6A461D19ED8D +functions\GroupAction\fnc_doGroupStaticPack.sqfc:172834:4140:7F1840E13A7C060007478451CBC40DBF +functions\GroupAction\fnc_doGroupSuppress.sqf:176974:1660:4571F76BC3ECA04D39C870D87F662B7B +functions\GroupAction\fnc_doGroupSuppress.sqfc:178634:2271:F219A12618BAD15846B747E68C8CB473 +functions\GroupAction\script_component.hpp:180905:54:6CCE9C47B1794B440210AFE451B227F3 +functions\script_component.hpp:180959:1674:6C818BE2E1A844391B7C56A5BACB409B +functions\UnitAction\fnc_doAssault.sqf:182633:2409:4643604854D185431ECFC04E47E0B993 +functions\UnitAction\fnc_doAssault.sqfc:185042:2857:39DF87B25B3385A497A1A5E3F35623BB +functions\UnitAction\fnc_doAssaultCQB.sqf:187899:3689:812F62DBF0A037238BC6F3C952B8ADB7 +functions\UnitAction\fnc_doAssaultCQB.sqfc:191588:4257:6E4D3BF460A03F245E51EC99DF36D986 +functions\UnitAction\fnc_doAssaultMemory.sqf:195845:2575:A817071761005081E05C64C879793A90 +functions\UnitAction\fnc_doAssaultMemory.sqfc:198420:3219:5C72DB8B3DE4CA41049107699E6E4B33 +functions\UnitAction\fnc_doAssaultSpeed.sqf:201639:849:BCBD4783703D0FD7908065BD2B58E5D5 +functions\UnitAction\fnc_doAssaultSpeed.sqfc:202488:1275:75813FC01333DC14869C50FD5F0DFFA5 +functions\UnitAction\fnc_doCallArtillery.sqf:203763:1958:67605BB3ED4FA120D5E17E8531AEC12D +functions\UnitAction\fnc_doCallArtillery.sqfc:205721:2448:CDF1C706501FDDCE407142E1E011CF7A +functions\UnitAction\fnc_doCheckBody.sqf:208169:1841:A902B901C5311355BA424E3047CA5540 +functions\UnitAction\fnc_doCheckBody.sqfc:210010:2251:C26F9D32117FF9B3F83ECEB4D91DD0A2 +functions\UnitAction\fnc_doCover.sqf:212261:1392:7D26369D093AF8D7682F87C90FB43B93 +functions\UnitAction\fnc_doCover.sqfc:213653:1902:BBA503899ABD6318F461EBA1ACF71AF1 +functions\UnitAction\fnc_doDodge.sqf:215555:2138:21B1F1B7002E5AEC4E5A44D7BDEC44CB +functions\UnitAction\fnc_doDodge.sqfc:217693:2741:4E28BE43D9D4EE5AB530A4441BF13CC3 +functions\UnitAction\fnc_doFleeing.sqf:220434:4040:E4FD8B7CBA13090864EA92944D5C8B2A +functions\UnitAction\fnc_doFleeing.sqfc:224474:4538:846B5B4E94185D0CCCDFD1C8B4A48835 +functions\UnitAction\fnc_doHide.sqf:229012:2449:C8369FFEF10B696F102ABDCFFDF70FF6 +functions\UnitAction\fnc_doHide.sqfc:231461:2914:5A9BB3DF82216FE9DFFFA5957C4895B7 +functions\UnitAction\fnc_doPanic.sqf:234375:1075:C850B3184FE1C84063E782BCCB004BCC +functions\UnitAction\fnc_doPanic.sqfc:235450:1392:ACB386692A52D49E89B29F4B01171B11 +functions\UnitAction\fnc_doReposition.sqf:236842:1368:54A32AA78323A4BEB9E4D9D25B8B38C8 +functions\UnitAction\fnc_doReposition.sqfc:238210:1765:22A2DC21B7400DE59B546F07B5A76848 +functions\UnitAction\fnc_doSmoke.sqf:239975:2226:47F82D03E3DA24CB5AEF3803B93210BD +functions\UnitAction\fnc_doSmoke.sqfc:242201:2656:EA2BAB3E6CB4B82ACD2A44E0058C32BD +functions\UnitAction\fnc_doSuppress.sqf:244857:2244:695ECCFF70796AC572575AA1E163EA8A +functions\UnitAction\fnc_doSuppress.sqfc:247101:3117:722752D116A44FBC1087447650D60687 +functions\UnitAction\fnc_doUGL.sqf:250218:3142:A6C9FBAFDC41733635ACE857895ABF88 +functions\UnitAction\fnc_doUGL.sqfc:253360:3671:81911EF929103D3C7E6027068079D2AA +functions\UnitAction\script_component.hpp:257031:54:6CCE9C47B1794B440210AFE451B227F3 +functions\VehicleAction\fnc_doSelectWarhead.sqf:257085:4039:64EA81D906E765B8AFCEE02713017B6B +functions\VehicleAction\fnc_doSelectWarhead.sqfc:261124:3494:BE58CFBA00C79277A671C4F5DC9A0AB6 +functions\VehicleAction\fnc_doVehicleAssault.sqf:264618:3495:18D4D9D0A10C013A166F5BD1A5FE562B +functions\VehicleAction\fnc_doVehicleAssault.sqfc:268113:4140:47B79F46199F9611AF3CC34D6236E708 +functions\VehicleAction\fnc_doVehicleJink.sqf:272253:2293:0BA3960FFDDE107DA2A5AE06A337D40F +functions\VehicleAction\fnc_doVehicleJink.sqfc:274546:2472:F8341A0476DF266CA5B339FE6A727E5D +functions\VehicleAction\fnc_doVehicleRotate.sqf:277018:2357:BB43FDB71D82D79058CD52E5C4B73395 +functions\VehicleAction\fnc_doVehicleRotate.sqfc:279375:2845:FCABA725FDDE768D00E1FE52192D1C78 +functions\VehicleAction\fnc_doVehicleSuppress.sqf:282220:2438:B73A0B01C0F4BCA8685DE49A256FB298 +functions\VehicleAction\fnc_doVehicleSuppress.sqfc:284658:3165:C4E8CB8F10CFE0123F0D2A2FEB7FBA1A +functions\VehicleAction\script_component.hpp:287823:54:6CCE9C47B1794B440210AFE451B227F3 +LICENSE:287877:19533:D67DDD8E5A6674D3CF4BABA8690B0435 +script_component.hpp:307410:378:81FEE3003FE33ED7B0E0CCA7048067AD +script_macros.hpp:307788:1649:7078125D1AEA75121B5FE440AC108CAD +script_mod.hpp:309437:464:8ABEFB1F30A495F7EF62ED7AE0BEA495 +script_version.hpp:309901:67:A04B40744F097331C408ED808AC38BCE +settings.sqf:309968:7510:4F7F7E23F223ED95B493EDE1F5BBE761 +stringtable.xml:317478:28491:5C86CE7E82ECF695986D9F5F088298EA +XEH_postInit.sqf:345969:4220:7B525F1EEEFB0F1AC2EDD7CC6E3790EA +XEH_postInit.sqfc:350189:4212:C073B30ABF69600462F6A76ED28E5A45 +XEH_preInit.sqf:354401:387:07835FE399E6BF4323E3A5605F6E2618 +XEH_preInit.sqfc:354788:8026:ED73E43F18AC018B9EC33EFC756C1280 +XEH_PREP.hpp:362814:1702:E12572F50CF65AD5CB7C252A7CCFEFBD +XEH_preStart.sqf:364516:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:364574:3528:58479A84F9713922EE54D5C5D35CDD40 +$$END$$:368102:21:A2604E54F372A00E34F8234DA4C2401A From 39119c9e354b702754b61a9561cc78016e785b84 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 03:56:34 -0300 Subject: [PATCH 16/34] srf: Introduce test that would catch the bug fixed by 32622dc3f61a4d6d74f1a46521575435839d7d10 --- src/pbo.rs | 2 +- src/srf.rs | 9 +++++++++ .../{ => @ace/addons}/ace_advanced_ballistics.pbo | Bin ...his_will_break_tests_if_we_order_incorrectly.txt | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) rename test_files/{ => @ace/addons}/ace_advanced_ballistics.pbo (100%) create mode 100644 test_files/@ace/addons/ace_advancedthis_will_break_tests_if_we_order_incorrectly.txt diff --git a/src/pbo.rs b/src/pbo.rs index 29673d7..c37cd47 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -135,7 +135,7 @@ mod tests { #[test] fn basic_pbo_test() { - let bytes = include_bytes!("../test_files/ace_advanced_ballistics.pbo"); + let bytes = include_bytes!("../test_files/@ace/addons/ace_advanced_ballistics.pbo"); let pbo = Pbo::read(Cursor::new(&bytes)).unwrap(); assert_eq!(pbo.entries.len(), 49); } diff --git a/src/srf.rs b/src/srf.rs index d35cd25..7a729ba 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -479,6 +479,7 @@ pub fn deserialize_legacy_srf(input: &mut I) -> Result()).unwrap(); + + assert_eq!(r#mod.checksum, Md5Digest::new("787662722D70C36DF28CD1D5EE8D8E86").unwrap()); + } } diff --git a/test_files/ace_advanced_ballistics.pbo b/test_files/@ace/addons/ace_advanced_ballistics.pbo similarity index 100% rename from test_files/ace_advanced_ballistics.pbo rename to test_files/@ace/addons/ace_advanced_ballistics.pbo diff --git a/test_files/@ace/addons/ace_advancedthis_will_break_tests_if_we_order_incorrectly.txt b/test_files/@ace/addons/ace_advancedthis_will_break_tests_if_we_order_incorrectly.txt new file mode 100644 index 0000000..54e2b78 --- /dev/null +++ b/test_files/@ace/addons/ace_advancedthis_will_break_tests_if_we_order_incorrectly.txt @@ -0,0 +1 @@ +this is a dummy file From 6cdfbd0710fd94eebc43835d31854db35a78817b Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 21 Nov 2022 04:41:57 -0300 Subject: [PATCH 17/34] meta: text is always LF Windows users: please use a text editor that doesn't require autocrlf --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f3dff49 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text eol=lf + +*.srf binary +*.pbo binary \ No newline at end of file From badb452123f70867f95d9507b330963c0c2e5a24 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Tue, 22 Nov 2022 07:46:19 -0300 Subject: [PATCH 18/34] mod_cache, gen_srf, sync: move responsibility of serde into mod_cache --- src/commands/gen_srf.rs | 3 +-- src/commands/sync.rs | 19 +++---------------- src/mod_cache.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index 35b04de..7f5ece7 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -35,6 +35,5 @@ pub fn gen_srf(base_path: &Path) { let cache = ModCache::new(mods); - let writer = BufWriter::new(File::create(base_path.join("nimble-cache.json")).unwrap()); - serde_json::to_writer(writer, &cache).unwrap(); + cache.to_disk(base_path).unwrap(); } diff --git a/src/commands/sync.rs b/src/commands/sync.rs index c2f27a4..3dec338 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -35,6 +35,8 @@ pub enum Error { LegacySrfDeserialization { source: srf::Error }, #[snafu(display("Failed to generate SRF: {}", source))] SrfGeneration { source: srf::Error }, + #[snafu(display("Failed to open ModCache: {}", source))] + ModCacheOpen { source: crate::mod_cache::Error } } fn diff_repo<'a>( @@ -206,22 +208,7 @@ pub fn sync( let remote_repo = repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)) .context(RepositoryFetchSnafu)?; - let mut mod_cache = { - let file = File::open(base_path.join("nimble-cache.json")); - - match file { - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Ok(ModCache::new_empty()) - } else { - return Err(Error::Io { source: e }); - } - } - Ok(file) => { - serde_json::from_reader(BufReader::new(file)).context(SrfDeserializationSnafu) - } - } - }?; + let mut mod_cache = ModCache::from_disk_or_empty(base_path).context(ModCacheOpenSnafu)?; let check = diff_repo(&mod_cache, &remote_repo); diff --git a/src/mod_cache.rs b/src/mod_cache.rs index 4debafe..8394bf5 100644 --- a/src/mod_cache.rs +++ b/src/mod_cache.rs @@ -1,7 +1,23 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use std::path::Path; +use std::fs::File; +use std::io::{BufReader, BufWriter}; +use snafu::{Snafu, ResultExt}; use crate::md5_digest::Md5Digest; +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to create cache file: {}", source))] + FileCreation { source: std::io::Error }, + #[snafu(display("failed to open cache file: {}", source))] + FileOpen { source: std::io::Error }, + #[snafu(display("serde failed to serialize: {}", source))] + Serialization { source: serde_json::Error }, + #[snafu(display("serde failed to deserialize: {}", source))] + Deserialization { source: serde_json::Error }, +} + #[derive(Serialize, Deserialize)] pub struct ModCache { version: u32, @@ -20,6 +36,31 @@ impl ModCache { } } + pub fn from_disk_or_empty(repo_path: &Path) -> Result { + let path = repo_path.join("nimble-cache.json"); + let open_result = File::open(path); + match open_result { + Ok(file) => { + let reader = BufReader::new(file); + serde_json::from_reader(reader).context(DeserializationSnafu) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + Ok(Self::new_empty()) + }, + Err(e) => Err(Error::FileOpen { source: e }) + } + } + + pub fn to_disk(&self, repo_path: &Path) -> Result<(), Error> { + let path = repo_path.join("nimble-cache.json"); + let file = File::create(path).context(FileCreationSnafu)?; + let writer = BufWriter::new(file); + + serde_json::to_writer(writer, &self).context(SerializationSnafu)?; + + Ok(()) + } + pub fn update_mod_checksum(&mut self, old_checksum: &Md5Digest, new_checksum: Md5Digest) { self.mods.remove(old_checksum); self.mods.insert(new_checksum); From d6339592a7e139cd2bcaca8ddc98e24bf881063c Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Tue, 22 Nov 2022 08:00:35 -0300 Subject: [PATCH 19/34] mod_cache: add mod name to cache Will be used for the launch command --- src/commands/gen_srf.rs | 14 ++++++++------ src/commands/sync.rs | 12 +++++++----- src/mod_cache.rs | 41 +++++++++++++++++++++++++++-------------- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index 7f5ece7..4e82f00 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -1,14 +1,14 @@ +use crate::md5_digest::Md5Digest; use crate::mod_cache::ModCache; use crate::srf; use rayon::prelude::*; -use std::collections::HashSet; +use std::collections::HashMap; use std::fs::File; use std::io::BufWriter; use std::path::Path; use walkdir::WalkDir; -use crate::md5_digest::Md5Digest; -pub fn gen_srf_for_mod(mod_path: &Path) -> Md5Digest { +pub fn gen_srf_for_mod(mod_path: &Path) -> srf::Mod { let generated_srf = srf::scan_mod(mod_path).unwrap(); let path = mod_path.join("mod.srf"); @@ -16,11 +16,11 @@ pub fn gen_srf_for_mod(mod_path: &Path) -> Md5Digest { let writer = BufWriter::new(File::create(path).unwrap()); serde_json::to_writer(writer, &generated_srf).unwrap(); - generated_srf.checksum + generated_srf } pub fn gen_srf(base_path: &Path) { - let mods: HashSet = WalkDir::new(base_path) + let mods: HashMap = WalkDir::new(base_path) .min_depth(1) .max_depth(1) .into_iter() @@ -29,7 +29,9 @@ pub fn gen_srf(base_path: &Path) { .filter(|e| e.file_type().is_dir() && e.file_name().to_string_lossy().starts_with('@')) .map(|entry| { let path = entry.path(); - gen_srf_for_mod(path) + let srf = gen_srf_for_mod(path); + + (srf.checksum.clone(), srf) }) .collect(); diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 3dec338..29a304d 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -36,7 +36,7 @@ pub enum Error { #[snafu(display("Failed to generate SRF: {}", source))] SrfGeneration { source: srf::Error }, #[snafu(display("Failed to open ModCache: {}", source))] - ModCacheOpen { source: crate::mod_cache::Error } + ModCacheOpen { source: crate::mod_cache::Error }, } fn diff_repo<'a>( @@ -49,7 +49,7 @@ fn diff_repo<'a>( // generate them for comparison. they aren't that useful anyway for _mod in &remote_repo.required_mods { - if !mod_cache.mods.contains(&_mod.checksum) { + if !mod_cache.mods.contains_key(&_mod.checksum) { downloads.push(_mod); } } @@ -192,7 +192,9 @@ fn execute_command_list( .context(IoSnafu)?; let mut local_file = File::create(&file_path).context(IoSnafu)?; - temp_download_file.seek(SeekFrom::Start(0)).context(IoSnafu)?; + temp_download_file + .seek(SeekFrom::Start(0)) + .context(IoSnafu)?; std::io::copy(&mut temp_download_file, &mut local_file).context(IoSnafu)?; } @@ -235,9 +237,9 @@ pub fn sync( // gen_srf for the mods we downloaded for _mod in &check { - let checksum = gen_srf_for_mod(&base_path.join(Path::new(&_mod.mod_name))); + let srf = gen_srf_for_mod(&base_path.join(Path::new(&_mod.mod_name))); - mod_cache.update_mod_checksum(&_mod.checksum, checksum); + mod_cache.update_mod_checksum(&_mod.checksum, srf.checksum); } // reserialize the cache diff --git a/src/mod_cache.rs b/src/mod_cache.rs index 8394bf5..5ec23f8 100644 --- a/src/mod_cache.rs +++ b/src/mod_cache.rs @@ -1,10 +1,10 @@ +use crate::md5_digest::Md5Digest; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::path::Path; +use snafu::{ResultExt, Snafu}; +use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, BufWriter}; -use snafu::{Snafu, ResultExt}; -use crate::md5_digest::Md5Digest; +use std::path::Path; #[derive(Debug, Snafu)] pub enum Error { @@ -18,21 +18,34 @@ pub enum Error { Deserialization { source: serde_json::Error }, } +#[derive(Serialize, Deserialize)] +pub struct Mod { + name: String, +} + +type SrfMod = crate::srf::Mod; + #[derive(Serialize, Deserialize)] pub struct ModCache { version: u32, - pub mods: HashSet, + pub mods: HashMap, } impl ModCache { - pub fn new(mods: HashSet) -> Self { - Self { version: 1, mods } + pub fn new(mods: HashMap) -> Self { + Self { + version: 1, + mods: mods + .into_iter() + .map(|(k, v)| (k, Mod { name: v.name })) + .collect(), + } } pub fn new_empty() -> Self { Self { version: 1, - mods: HashSet::new(), + mods: HashMap::new(), } } @@ -44,10 +57,8 @@ impl ModCache { let reader = BufReader::new(file); serde_json::from_reader(reader).context(DeserializationSnafu) } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - Ok(Self::new_empty()) - }, - Err(e) => Err(Error::FileOpen { source: e }) + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::new_empty()), + Err(e) => Err(Error::FileOpen { source: e }), } } @@ -62,7 +73,9 @@ impl ModCache { } pub fn update_mod_checksum(&mut self, old_checksum: &Md5Digest, new_checksum: Md5Digest) { - self.mods.remove(old_checksum); - self.mods.insert(new_checksum); + let r#mod = self.mods.remove(old_checksum); + if let Some(r#mod) = r#mod { + self.mods.insert(new_checksum, r#mod); + } } } From 1cc3d42ea5063c5f612f56dc65868fd9a141cfae Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Tue, 22 Nov 2022 08:45:45 -0300 Subject: [PATCH 20/34] wip launch --- src/commands/launch.rs | 76 ++++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + src/main.rs | 17 +++++++--- src/mod_cache.rs | 2 +- 4 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 src/commands/launch.rs diff --git a/src/commands/launch.rs b/src/commands/launch.rs new file mode 100644 index 0000000..9c6c253 --- /dev/null +++ b/src/commands/launch.rs @@ -0,0 +1,76 @@ +use crate::mod_cache; +use crate::mod_cache::ModCache; +use snafu::{ResultExt, Snafu}; +use std::path::{PathBuf, Path}; +use std::cfg; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to open ModCache: {}", source))] + ModCacheOpen { source: mod_cache::Error }, +} + +fn generate_mod_args(base_path: &Path, mod_cache: &ModCache) -> String { + mod_cache.mods.values().fold(String::from("-mod="), |acc, r#mod| { + let mod_name = &r#mod.name; + let full_path = base_path.join(Path::new(mod_name)).to_string_lossy().to_string(); + format!("{acc}{full_path};") + }) +} + +// if we're on windows we don't have to do anything +#[cfg(windows)] +fn convert_host_base_path_to_proton_base_path(host_base_path: PathBuf) -> PathBuf { + host_base_path +} + +// if we're not on windows, try to find a "drive_c" dir in the ancestors of base_path +#[cfg(not(windows))] +fn convert_host_base_path_to_proton_base_path(host_base_path: PathBuf) -> PathBuf { + let drive_c_path = host_base_path.ancestors().find(|&x| x.ends_with("drive_c")).unwrap(); + + let relative = host_base_path.strip_prefix(drive_c_path).unwrap(); + + Path::new("c:/").join(relative) +} + +pub fn launch(base_path: PathBuf) -> Result<(), Error> { + let mod_cache = ModCache::from_disk_or_empty(&base_path).context(ModCacheOpenSnafu)?; + + let proton_base_path = convert_host_base_path_to_proton_base_path(base_path); + + let cmdline = mod_cache.mods.values().fold(String::from("-mod="), |acc, r#mod| { + let mod_name = &r#mod.name; + let full_path = proton_base_path.join(Path::new(mod_name)).to_string_lossy().to_string(); + format!("{acc}{full_path};") + }); + + dbg!(cmdline); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(windows)] + fn test_proton_path_conversion() { + // on windows, this should do nothing + let original_path = PathBuf::from("C:\\random\\paths\\drive_c\\banana_repo"); + let converted = convert_host_base_path_to_proton_base_path(original_path.clone()); + + assert_eq!(original_path, converted); + } + + #[test] + #[cfg(not(windows))] + fn test_proton_path_conversion() { + // on windows, this should do nothing + let original_path = PathBuf::from("/home/random/paths/drive_c/banana_repo"); + let converted = convert_host_base_path_to_proton_base_path(original_path.clone()); + + assert_eq!(converted, PathBuf::from("c:/banana_repo")); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index dd933fd..4b6307e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod gen_srf; +pub mod launch; pub mod sync; diff --git a/src/main.rs b/src/main.rs index 2a8353c..fd4d389 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,11 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; mod commands; +mod md5_digest; mod mod_cache; mod pbo; mod repository; mod srf; -mod md5_digest; #[derive(Subcommand)] enum Commands { @@ -16,7 +16,7 @@ enum Commands { repo_url: String, #[clap(short, long)] - local_path: PathBuf, + path: PathBuf, #[clap(short, long)] dry_run: bool, @@ -24,7 +24,11 @@ enum Commands { GenSrf { #[clap(short, long)] path: PathBuf, - } + }, + Launch { + #[clap(short, long)] + path: PathBuf, + }, } #[derive(Parser)] @@ -43,13 +47,16 @@ fn main() { match args.command { Commands::Sync { repo_url, - local_path, + path, dry_run, } => { - commands::sync::sync(&mut agent, &repo_url, &local_path, dry_run).unwrap(); + commands::sync::sync(&mut agent, &repo_url, &path, dry_run).unwrap(); } Commands::GenSrf { path } => { commands::gen_srf::gen_srf(&path); } + Commands::Launch { path } => { + commands::launch::launch(path).unwrap(); + } } } diff --git a/src/mod_cache.rs b/src/mod_cache.rs index 5ec23f8..b7961d7 100644 --- a/src/mod_cache.rs +++ b/src/mod_cache.rs @@ -20,7 +20,7 @@ pub enum Error { #[derive(Serialize, Deserialize)] pub struct Mod { - name: String, + pub name: String, } type SrfMod = crate::srf::Mod; From 2dd6ef0bc3cbd9f7dadf0afd51d15eece249ab5c Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Wed, 23 Nov 2022 00:26:13 -0300 Subject: [PATCH 21/34] more launch work steam protocol is horribly unreliable. might have to launch game directly. --- Cargo.lock | 75 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/commands/launch.rs | 59 ++++++++++++++++++++------------- 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6111b8..c442492 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -379,6 +379,8 @@ dependencies = [ "hex", "indicatif", "md-5", + "open", + "percent-encoding", "rayon", "relative-path", "serde", @@ -411,12 +413,28 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys", +] + [[package]] name = "os_str_bytes" version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "2.2.0" @@ -902,3 +920,60 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 5b5e61b..8811a89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,6 @@ walkdir = "2" indicatif = "0.17" tempfile = "3" hex = "0.4" +open = "3" +percent-encoding = "2" # tinyvec = { version = "1.5", features = ["alloc", "rustc_1_55"] } diff --git a/src/commands/launch.rs b/src/commands/launch.rs index 9c6c253..bd8cae4 100644 --- a/src/commands/launch.rs +++ b/src/commands/launch.rs @@ -1,51 +1,66 @@ use crate::mod_cache; use crate::mod_cache::ModCache; -use snafu::{ResultExt, Snafu}; -use std::path::{PathBuf, Path}; +use snafu::{OptionExt, ResultExt, Snafu}; use std::cfg; +use std::path::{Path, PathBuf}; #[derive(Debug, Snafu)] pub enum Error { #[snafu(display("failed to open ModCache: {}", source))] ModCacheOpen { source: mod_cache::Error }, + #[snafu(display("failed to find drive_c"))] + FailedToFindDriveC, } fn generate_mod_args(base_path: &Path, mod_cache: &ModCache) -> String { - mod_cache.mods.values().fold(String::from("-mod="), |acc, r#mod| { - let mod_name = &r#mod.name; - let full_path = base_path.join(Path::new(mod_name)).to_string_lossy().to_string(); - format!("{acc}{full_path};") - }) + mod_cache + .mods + .values() + .fold(String::from("-noLauncher -mod="), |acc, r#mod| { + let mod_name = &r#mod.name; + let full_path = base_path + .join(Path::new(mod_name)) + .to_string_lossy() + .to_string(); + format!("{acc}{full_path};") + }) } // if we're on windows we don't have to do anything #[cfg(windows)] -fn convert_host_base_path_to_proton_base_path(host_base_path: PathBuf) -> PathBuf { - host_base_path +fn convert_host_base_path_to_proton_base_path(host_base_path: &Path) -> Result { + Ok(host_base_path.to_owned()) } // if we're not on windows, try to find a "drive_c" dir in the ancestors of base_path #[cfg(not(windows))] -fn convert_host_base_path_to_proton_base_path(host_base_path: PathBuf) -> PathBuf { - let drive_c_path = host_base_path.ancestors().find(|&x| x.ends_with("drive_c")).unwrap(); +fn convert_host_base_path_to_proton_base_path(host_base_path: &Path) -> Result { + let drive_c_path = host_base_path + .ancestors() + .find(|&x| x.ends_with("drive_c")) + .context(FailedToFindDriveCSnafu)?; - let relative = host_base_path.strip_prefix(drive_c_path).unwrap(); + let relative = host_base_path + .strip_prefix(drive_c_path) + .expect("drive_c_path was not a prefix of host_base_path, this should never happen"); - Path::new("c:/").join(relative) + Ok(Path::new("c:/").join(relative)) } pub fn launch(base_path: PathBuf) -> Result<(), Error> { let mod_cache = ModCache::from_disk_or_empty(&base_path).context(ModCacheOpenSnafu)?; - let proton_base_path = convert_host_base_path_to_proton_base_path(base_path); + let proton_base_path = convert_host_base_path_to_proton_base_path(&base_path)?; - let cmdline = mod_cache.mods.values().fold(String::from("-mod="), |acc, r#mod| { - let mod_name = &r#mod.name; - let full_path = proton_base_path.join(Path::new(mod_name)).to_string_lossy().to_string(); - format!("{acc}{full_path};") - }); + let binding = generate_mod_args(&proton_base_path, &mod_cache); + let cmdline = + percent_encoding::utf8_percent_encode(&binding, percent_encoding::NON_ALPHANUMERIC); - dbg!(cmdline); + let steam_url = format!("steam://run/107410//{cmdline}/"); + + dbg!(&steam_url); + + open::that(steam_url).unwrap(); Ok(()) } @@ -59,7 +74,7 @@ mod tests { fn test_proton_path_conversion() { // on windows, this should do nothing let original_path = PathBuf::from("C:\\random\\paths\\drive_c\\banana_repo"); - let converted = convert_host_base_path_to_proton_base_path(original_path.clone()); + let converted = convert_host_base_path_to_proton_base_path(&original_path).unwrap(); assert_eq!(original_path, converted); } @@ -69,7 +84,7 @@ mod tests { fn test_proton_path_conversion() { // on windows, this should do nothing let original_path = PathBuf::from("/home/random/paths/drive_c/banana_repo"); - let converted = convert_host_base_path_to_proton_base_path(original_path.clone()); + let converted = convert_host_base_path_to_proton_base_path(&original_path).unwrap(); assert_eq!(converted, PathBuf::from("c:/banana_repo")); } From c3bac3da97e6606fdbe6c37c3d918273d8a71aac Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 12 Dec 2022 00:02:16 -0300 Subject: [PATCH 22/34] sync: fix checksum caching after changes to a mod --- src/commands/sync.rs | 7 ++++++- src/mod_cache.rs | 21 ++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 29a304d..ff04a18 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -216,6 +216,11 @@ pub fn sync( println!("mods to check: {:#?}", check); + // remove all mods to check from cache, we'll readd them later + for _mod in &check { + mod_cache.remove(&_mod.checksum); + } + let mut download_commands = vec![]; for _mod in &check { @@ -239,7 +244,7 @@ pub fn sync( for _mod in &check { let srf = gen_srf_for_mod(&base_path.join(Path::new(&_mod.mod_name))); - mod_cache.update_mod_checksum(&_mod.checksum, srf.checksum); + mod_cache.insert(srf); } // reserialize the cache diff --git a/src/mod_cache.rs b/src/mod_cache.rs index b7961d7..2233b4c 100644 --- a/src/mod_cache.rs +++ b/src/mod_cache.rs @@ -18,11 +18,17 @@ pub enum Error { Deserialization { source: serde_json::Error }, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Mod { pub name: String, } +impl From for Mod { + fn from(value: crate::srf::Mod) -> Self { + Mod { name: value.name } + } +} + type SrfMod = crate::srf::Mod; #[derive(Serialize, Deserialize)] @@ -37,7 +43,7 @@ impl ModCache { version: 1, mods: mods .into_iter() - .map(|(k, v)| (k, Mod { name: v.name })) + .map(|(k, v)| (k, v.into())) .collect(), } } @@ -72,10 +78,11 @@ impl ModCache { Ok(()) } - pub fn update_mod_checksum(&mut self, old_checksum: &Md5Digest, new_checksum: Md5Digest) { - let r#mod = self.mods.remove(old_checksum); - if let Some(r#mod) = r#mod { - self.mods.insert(new_checksum, r#mod); - } + pub fn remove(&mut self, checksum: &Md5Digest) { + self.mods.remove(checksum); + } + + pub fn insert(&mut self, r#mod: crate::srf::Mod) { + self.mods.insert(r#mod.checksum.clone(), r#mod.into()); } } From fa5d42db8c771cc807c891a4ba5e19bed312af7f Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Fri, 29 Dec 2023 02:16:47 -0300 Subject: [PATCH 23/34] pbo: Make CString deserialization safe. --- src/pbo.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pbo.rs b/src/pbo.rs index c37cd47..69f1f00 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -1,8 +1,9 @@ use std::{ collections::HashMap, - ffi::CStr, + ffi::CString, io::{BufRead, Seek}, }; +use std::ffi::FromVecWithNulError; use byteorder::{LittleEndian, ReadBytesExt}; use snafu::{ResultExt, Snafu}; @@ -39,6 +40,8 @@ pub enum Error { Io { source: std::io::Error }, #[snafu(display("unknown pbo type: {}", r#type))] PboType { r#type: u32 }, + #[snafu(display("string deserialization error: {}", source))] + StringDeserialization { source: FromVecWithNulError } } fn read_string(input: &mut I) -> Result { @@ -46,9 +49,9 @@ fn read_string(input: &mut I) -> Result { input.read_until(b'\0', &mut buf).context(IoSnafu {})?; - let str = unsafe { CStr::from_bytes_with_nul_unchecked(&buf) }.to_string_lossy(); + let cstring = CString::from_vec_with_nul(buf).context(StringDeserializationSnafu)?; - Ok(str.to_string()) + Ok(cstring.to_string_lossy().to_string()) } impl PboEntry { From 2d89f8c9b5a25d9087da93a2690c16c99b06095c Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Fri, 29 Dec 2023 02:38:02 -0300 Subject: [PATCH 24/34] sync: Cleanup files --- src/commands/sync.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/commands/sync.rs b/src/commands/sync.rs index ff04a18..e1fb3e9 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -149,9 +149,25 @@ fn diff_mod( } } + // remove any local files that remain here + remove_leftover_files(local_base_path, &remote_srf, local_files.into_values()).context(IoSnafu)?; + Ok(download_list) } +// remove files that are present in the local disk but not in the remote repo +fn remove_leftover_files<'a>(local_base_path: &Path, r#mod: &srf::Mod, files: impl Iterator) -> Result<(), std::io::Error> { + for file in files { + let path = file.path.to_path(local_base_path.join(Path::new(&r#mod.name))); + + println!("removing leftover file {}", &path.display()); + + std::fs::remove_file(&path)?; + } + + Ok(()) +} + fn execute_command_list( agent: &mut ureq::Agent, remote_base: &str, From dfca1d21b417ff6000ba14b8eba90bd8eb156985 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Fri, 29 Dec 2023 15:50:22 -0300 Subject: [PATCH 25/34] sync: Properly report errors on deserialization failure --- src/commands/sync.rs | 19 ++++++++++++------- src/srf.rs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/commands/sync.rs b/src/commands/sync.rs index e1fb3e9..c2ac2ce 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -79,10 +79,13 @@ fn diff_mod( // yeet utf-8 bom, which is bad, not very useful and not supported by serde let bomless = buf.trim_start_matches('\u{feff}'); - let remote_srf: srf::Mod = serde_json::from_str(bomless) - .context(SrfDeserializationSnafu) - .or_else(|_| srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(bomless)))) - .context(LegacySrfDeserializationSnafu)?; + let remote_is_legacy = srf::is_legacy_srf(&mut Cursor::new(bomless)).context(IoSnafu)?; + + let remote_srf: srf::Mod = if remote_is_legacy { + srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(bomless))).context(LegacySrfDeserializationSnafu)? + } else { + serde_json::from_str(bomless).context(SrfDeserializationSnafu)? + }; let local_path = local_base_path.join(Path::new(&format!("{}/", remote_mod.mod_name))); let srf_path = local_path.join(Path::new("mod.srf")); @@ -97,9 +100,11 @@ fn diff_mod( Ok(file) => { let mut reader = BufReader::new(file); - serde_json::from_reader(&mut reader) - .or_else(|_| srf::deserialize_legacy_srf(&mut reader)) - .context(LegacySrfDeserializationSnafu)? + if srf::is_legacy_srf(&mut reader).context(IoSnafu)? { + srf::deserialize_legacy_srf(&mut reader).context(LegacySrfDeserializationSnafu)? + } else { + serde_json::from_reader(&mut reader).context(SrfDeserializationSnafu)? + } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { srf::scan_mod(&local_path).context(SrfGenerationSnafu)? diff --git a/src/srf.rs b/src/srf.rs index 7a729ba..89e6e31 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -448,6 +448,22 @@ fn read_legacy_srf_file( }) } +pub fn is_legacy_srf(input: &mut I) -> Result { + let start = input.stream_position()?; + let mut buf = [0; 5]; + input.read_exact(&mut buf)?; + + let output = if String::from_utf8_lossy(&buf) == "ADDON" { + true + } else { + false + }; + + input.seek(SeekFrom::Start(start))?; + + Ok(output) +} + pub fn deserialize_legacy_srf(input: &mut I) -> Result { // swifty's legacy srf format is stateful input.seek(SeekFrom::Start(0)).context(IoSnafu)?; From 6d2a360c40fed5711c491ab9da5b3e627501da33 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Fri, 29 Dec 2023 15:54:34 -0300 Subject: [PATCH 26/34] cargo fmt --- src/commands/sync.rs | 19 ++++++++++++++----- src/main.rs | 2 +- src/md5_digest.rs | 34 ++++++++++++++++++---------------- src/mod_cache.rs | 5 +---- src/pbo.rs | 6 +++--- src/repository.rs | 2 +- src/srf.rs | 21 ++++++++++++++++----- 7 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/commands/sync.rs b/src/commands/sync.rs index c2ac2ce..bb53a40 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -82,7 +82,8 @@ fn diff_mod( let remote_is_legacy = srf::is_legacy_srf(&mut Cursor::new(bomless)).context(IoSnafu)?; let remote_srf: srf::Mod = if remote_is_legacy { - srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(bomless))).context(LegacySrfDeserializationSnafu)? + srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(bomless))) + .context(LegacySrfDeserializationSnafu)? } else { serde_json::from_str(bomless).context(SrfDeserializationSnafu)? }; @@ -101,7 +102,8 @@ fn diff_mod( let mut reader = BufReader::new(file); if srf::is_legacy_srf(&mut reader).context(IoSnafu)? { - srf::deserialize_legacy_srf(&mut reader).context(LegacySrfDeserializationSnafu)? + srf::deserialize_legacy_srf(&mut reader) + .context(LegacySrfDeserializationSnafu)? } else { serde_json::from_reader(&mut reader).context(SrfDeserializationSnafu)? } @@ -155,15 +157,22 @@ fn diff_mod( } // remove any local files that remain here - remove_leftover_files(local_base_path, &remote_srf, local_files.into_values()).context(IoSnafu)?; + remove_leftover_files(local_base_path, &remote_srf, local_files.into_values()) + .context(IoSnafu)?; Ok(download_list) } // remove files that are present in the local disk but not in the remote repo -fn remove_leftover_files<'a>(local_base_path: &Path, r#mod: &srf::Mod, files: impl Iterator) -> Result<(), std::io::Error> { +fn remove_leftover_files<'a>( + local_base_path: &Path, + r#mod: &srf::Mod, + files: impl Iterator, +) -> Result<(), std::io::Error> { for file in files { - let path = file.path.to_path(local_base_path.join(Path::new(&r#mod.name))); + let path = file + .path + .to_path(local_base_path.join(Path::new(&r#mod.name))); println!("removing leftover file {}", &path.display()); diff --git a/src/main.rs b/src/main.rs index fd4d389..a2882c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,7 @@ fn main() { Commands::GenSrf { path } => { commands::gen_srf::gen_srf(&path); } - Commands::Launch { path } => { + Commands::Launch { path } => { commands::launch::launch(path).unwrap(); } } diff --git a/src/md5_digest.rs b/src/md5_digest.rs index f444722..83fb0a8 100644 --- a/src/md5_digest.rs +++ b/src/md5_digest.rs @@ -1,12 +1,12 @@ -use std::fmt::{Debug, Formatter}; use hex::FromHexError; -use serde::{Serialize, Deserialize, Serializer, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use snafu::{ResultExt, Snafu}; +use std::fmt::{Debug, Formatter}; #[derive(Debug, Snafu)] pub enum Error { #[snafu(display("hex digest decode error: {}", source))] - HexDecode { source: FromHexError } + HexDecode { source: FromHexError }, } #[derive(Default, Hash, PartialEq, Eq, Clone)] @@ -15,24 +15,23 @@ pub struct Md5Digest { } impl Md5Digest { - pub fn new(digest: &str) -> Result { - let mut inner = [0; 16]; - hex::decode_to_slice(digest, &mut inner).context(HexDecodeSnafu)?; + pub fn new(digest: &str) -> Result { + let mut inner = [0; 16]; + hex::decode_to_slice(digest, &mut inner).context(HexDecodeSnafu)?; - Ok(Self { - inner, - }) - } + Ok(Self { inner }) + } pub fn from_bytes(bytes: [u8; 16]) -> Self { - Self { - inner: bytes, - } + Self { inner: bytes } } } impl Serialize for Md5Digest { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { let digest = hex::encode_upper(&self.inner); serializer.serialize_str(&digest) @@ -40,7 +39,10 @@ impl Serialize for Md5Digest { } impl<'de> Deserialize<'de> for Md5Digest { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { let digest = String::deserialize(deserializer)?; let mut inner = [0; 16]; @@ -56,4 +58,4 @@ impl Debug for Md5Digest { .field("inner", &hex::encode_upper(&self.inner)) .finish() } -} \ No newline at end of file +} diff --git a/src/mod_cache.rs b/src/mod_cache.rs index 2233b4c..f0ad8c3 100644 --- a/src/mod_cache.rs +++ b/src/mod_cache.rs @@ -41,10 +41,7 @@ impl ModCache { pub fn new(mods: HashMap) -> Self { Self { version: 1, - mods: mods - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect(), + mods: mods.into_iter().map(|(k, v)| (k, v.into())).collect(), } } diff --git a/src/pbo.rs b/src/pbo.rs index 69f1f00..d43fec9 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -1,9 +1,9 @@ +use std::ffi::FromVecWithNulError; use std::{ collections::HashMap, ffi::CString, io::{BufRead, Seek}, }; -use std::ffi::FromVecWithNulError; use byteorder::{LittleEndian, ReadBytesExt}; use snafu::{ResultExt, Snafu}; @@ -41,7 +41,7 @@ pub enum Error { #[snafu(display("unknown pbo type: {}", r#type))] PboType { r#type: u32 }, #[snafu(display("string deserialization error: {}", source))] - StringDeserialization { source: FromVecWithNulError } + StringDeserialization { source: FromVecWithNulError }, } fn read_string(input: &mut I) -> Result { @@ -133,8 +133,8 @@ impl Pbo { #[cfg(test)] mod tests { - use std::io::Cursor; use super::*; + use std::io::Cursor; #[test] fn basic_pbo_test() { diff --git a/src/repository.rs b/src/repository.rs index ed82aa2..1a4d76c 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,7 +1,7 @@ +use crate::md5_digest::Md5Digest; use serde::{Deserialize, Deserializer, Serialize}; use snafu::prelude::*; use std::{fmt::Display, net::IpAddr, str::FromStr}; -use crate::md5_digest::Md5Digest; #[derive(Debug, Snafu)] pub enum Error { diff --git a/src/srf.rs b/src/srf.rs index 89e6e31..323b40c 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -1,3 +1,4 @@ +use crate::md5_digest::Md5Digest; use md5::{Digest, Md5}; use rayon::prelude::*; use relative_path::RelativePathBuf; @@ -11,7 +12,6 @@ use std::{ path::Path, }; use walkdir::WalkDir; -use crate::md5_digest::Md5Digest; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] @@ -504,14 +504,25 @@ mod tests { let deserialized = deserialize_legacy_srf(&mut cursor).unwrap(); assert_eq!(deserialized.name, "@lambs_danger"); - assert_eq!(deserialized.checksum, Md5Digest::new("44C1B8021822F80E1E560689D2AAB0BF").unwrap()); + assert_eq!( + deserialized.checksum, + Md5Digest::new("44C1B8021822F80E1E560689D2AAB0BF").unwrap() + ); } #[test] fn gen_srf_test() { let project_root = env!("CARGO_MANIFEST_DIR"); - let r#mod = scan_mod(&[project_root, "test_files", "@ace"].iter().collect::()).unwrap(); - - assert_eq!(r#mod.checksum, Md5Digest::new("787662722D70C36DF28CD1D5EE8D8E86").unwrap()); + let r#mod = scan_mod( + &[project_root, "test_files", "@ace"] + .iter() + .collect::(), + ) + .unwrap(); + + assert_eq!( + r#mod.checksum, + Md5Digest::new("787662722D70C36DF28CD1D5EE8D8E86").unwrap() + ); } } From 951d655c95d9016644384413baf256a4792ec996 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Sun, 31 Dec 2023 01:31:23 -0300 Subject: [PATCH 27/34] project-wide: cargo clippy --- src/commands/gen_srf.rs | 2 +- src/commands/launch.rs | 6 +++--- src/commands/sync.rs | 41 ++++++++++++++++++++--------------------- src/main.rs | 2 +- src/md5_digest.rs | 4 ++-- src/srf.rs | 35 +++++++++++++---------------------- 6 files changed, 40 insertions(+), 50 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index 4e82f00..1a46274 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -25,7 +25,7 @@ pub fn gen_srf(base_path: &Path) { .max_depth(1) .into_iter() .par_bridge() - .filter_map(|e| e.ok()) + .filter_map(Result::ok) .filter(|e| e.file_type().is_dir() && e.file_name().to_string_lossy().starts_with('@')) .map(|entry| { let path = entry.path(); diff --git a/src/commands/launch.rs b/src/commands/launch.rs index bd8cae4..bd7c853 100644 --- a/src/commands/launch.rs +++ b/src/commands/launch.rs @@ -47,10 +47,10 @@ fn convert_host_base_path_to_proton_base_path(host_base_path: &Path) -> Result

Result<(), Error> { - let mod_cache = ModCache::from_disk_or_empty(&base_path).context(ModCacheOpenSnafu)?; +pub fn launch(base_path: &Path) -> Result<(), Error> { + let mod_cache = ModCache::from_disk_or_empty(base_path).context(ModCacheOpenSnafu)?; - let proton_base_path = convert_host_base_path_to_proton_base_path(&base_path)?; + let proton_base_path = convert_host_base_path_to_proton_base_path(base_path)?; let binding = generate_mod_args(&proton_base_path, &mod_cache); let cmdline = diff --git a/src/commands/sync.rs b/src/commands/sync.rs index bb53a40..3219401 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -48,9 +48,9 @@ fn diff_repo<'a>( // repo checksums use the repo generation timestamp in the checksum calculation, so we can't really // generate them for comparison. they aren't that useful anyway - for _mod in &remote_repo.required_mods { - if !mod_cache.mods.contains_key(&_mod.checksum) { - downloads.push(_mod); + for r#mod in &remote_repo.required_mods { + if !mod_cache.mods.contains_key(&r#mod.checksum) { + downloads.push(r#mod); } } @@ -92,9 +92,7 @@ fn diff_mod( let srf_path = local_path.join(Path::new("mod.srf")); let local_srf = { - if !local_path.exists() { - srf::Mod::generate_invalid(&remote_srf) - } else { + if local_path.exists() { let file = File::open(srf_path); match file { @@ -113,6 +111,8 @@ fn diff_mod( } Err(e) => return Err(Error::Io { source: e }), } + } else { + srf::Mod::generate_invalid(&remote_srf) } }; @@ -145,14 +145,14 @@ fn diff_mod( file: format!("{}/{}", remote_srf.name, path), begin: 0, end: file.length, - }) + }); } } else { download_list.push(DownloadCommand { file: format!("{}/{}", remote_srf.name, path), begin: 0, end: file.length, - }) + }); } } @@ -204,8 +204,7 @@ fn execute_command_list( let pb = response .header("Content-Length") .and_then(|len| len.parse().ok()) - .map(ProgressBar::new) - .unwrap_or_else(ProgressBar::new_spinner); + .map_or_else(ProgressBar::new_spinner, ProgressBar::new); pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") .unwrap() @@ -237,27 +236,27 @@ pub fn sync( base_path: &Path, dry_run: bool, ) -> Result<(), Error> { - let remote_repo = repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)) + let remote_repo = repository::get_repository_info(agent, &format!("{repo_url}/repo.json")) .context(RepositoryFetchSnafu)?; let mut mod_cache = ModCache::from_disk_or_empty(base_path).context(ModCacheOpenSnafu)?; let check = diff_repo(&mod_cache, &remote_repo); - println!("mods to check: {:#?}", check); + println!("mods to check: {check:#?}"); - // remove all mods to check from cache, we'll readd them later - for _mod in &check { - mod_cache.remove(&_mod.checksum); + // remove all mods to check from cache, we'll read them later + for r#mod in &check { + mod_cache.remove(&r#mod.checksum); } let mut download_commands = vec![]; - for _mod in &check { - download_commands.extend(diff_mod(agent, repo_url, base_path, _mod).unwrap()); + for r#mod in &check { + download_commands.extend(diff_mod(agent, repo_url, base_path, r#mod).unwrap()); } - println!("download commands: {:#?}", download_commands); + println!("download commands: {download_commands:#?}"); if dry_run { return Ok(()); @@ -266,13 +265,13 @@ pub fn sync( let res = execute_command_list(agent, repo_url, base_path, &download_commands); if let Err(e) = res { - println!("an error occured while downloading: {}", e); + println!("an error occured while downloading: {e}"); println!("you should retry this command"); } // gen_srf for the mods we downloaded - for _mod in &check { - let srf = gen_srf_for_mod(&base_path.join(Path::new(&_mod.mod_name))); + for r#mod in &check { + let srf = gen_srf_for_mod(&base_path.join(Path::new(&r#mod.mod_name))); mod_cache.insert(srf); } diff --git a/src/main.rs b/src/main.rs index a2882c2..9b8c910 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,7 @@ fn main() { commands::gen_srf::gen_srf(&path); } Commands::Launch { path } => { - commands::launch::launch(path).unwrap(); + commands::launch::launch(&path).unwrap(); } } } diff --git a/src/md5_digest.rs b/src/md5_digest.rs index 83fb0a8..61ac67f 100644 --- a/src/md5_digest.rs +++ b/src/md5_digest.rs @@ -32,7 +32,7 @@ impl Serialize for Md5Digest { where S: Serializer, { - let digest = hex::encode_upper(&self.inner); + let digest = hex::encode_upper(self.inner); serializer.serialize_str(&digest) } @@ -55,7 +55,7 @@ impl<'de> Deserialize<'de> for Md5Digest { impl Debug for Md5Digest { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Md5Digest") - .field("inner", &hex::encode_upper(&self.inner)) + .field("inner", &hex::encode_upper(self.inner)) .finish() } } diff --git a/src/srf.rs b/src/srf.rs index 323b40c..8477742 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -87,7 +87,7 @@ pub struct Mod { impl Mod { pub fn generate_invalid(remote: &Self) -> Self { Self { - checksum: Default::default(), + checksum: Md5Digest::default(), files: vec![], ..remote.clone() } @@ -102,7 +102,7 @@ fn generate_hash(file: &mut BufReader, len: u64) -> Result Result { @@ -129,16 +129,16 @@ pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { // swifty, as always, does very strange things for entry in pbo.entries.iter().skip(1) { - let hash = generate_hash(pbo.input, entry.data_size as u64)?; + let hash = generate_hash(pbo.input, u64::from(entry.data_size))?; parts.push(Part { path: entry.filename.clone(), - length: entry.data_size as u64, + length: u64::from(entry.data_size), checksum: hash, start: offset, }); - offset += entry.data_size as u64; + offset += u64::from(entry.data_size); } { @@ -195,7 +195,7 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { let hash = hasher.finalize(); parts.push(Part { - checksum: format!("{:X}", hash), + checksum: format!("{hash:X}"), length: copied, path: format!( "{}_{}", @@ -207,7 +207,7 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { pos ), start: pre_copy_pos, - }) + }); } // final checksum generation @@ -215,7 +215,7 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { let mut hasher = Md5::new(); for part in &parts { - hasher.update(&part.checksum) + hasher.update(&part.checksum); } let path = RelativePathBuf::from_path(path.strip_prefix(base_path).unwrap()).unwrap(); @@ -235,7 +235,7 @@ fn recurse(path: &Path, base_path: &Path) -> Result, Error> { let entries: Vec<_> = WalkDir::new(path) .into_iter() .filter_entry(|e| e.file_name() != OsStr::new("mod.srf")) - .filter_map(|e| e.ok()) + .filter_map(Result::ok) .filter(|e| { // someday this spaghetti can just be replaced by Option::contains if let Some(is_dir) = e.metadata().ok().map(|metadata| metadata.is_dir()) { @@ -308,9 +308,7 @@ fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Error> { })? .to_string(); - if r#type != "ADDON" { - panic!("wrong magic"); - } + assert_eq!(r#type, "ADDON", "wrong magic"); let name = split .next() @@ -381,8 +379,8 @@ fn read_legacy_srf_part(line: &str) -> Result { Ok(Part { path, - start, length, + start, checksum, }) } @@ -440,10 +438,10 @@ fn read_legacy_srf_file( } Ok(File { - r#type, path, length, checksum, + r#type, parts, }) } @@ -452,16 +450,9 @@ pub fn is_legacy_srf(input: &mut I) -> Result { let start = input.stream_position()?; let mut buf = [0; 5]; input.read_exact(&mut buf)?; - - let output = if String::from_utf8_lossy(&buf) == "ADDON" { - true - } else { - false - }; - input.seek(SeekFrom::Start(start))?; - Ok(output) + Ok(String::from_utf8_lossy(&buf) == "ADDON") } pub fn deserialize_legacy_srf(input: &mut I) -> Result { From 2334c22600f9fac5f1184f5ecbc08c18a5c32d99 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 30 Dec 2024 23:13:32 -0300 Subject: [PATCH 28/34] sync, pbo: Explicitly annotate our dead code fields. Some of these could reasonably be removed, but they don't cost us much to keep around anyway. --- src/commands/sync.rs | 4 ++++ src/pbo.rs | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 3219401..197f1de 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -12,7 +12,11 @@ use tempfile::tempfile; #[derive(Debug)] struct DownloadCommand { file: String, + + // These are currently unused. TODO: implement file diffing. + #[allow(dead_code)] begin: u64, + #[allow(dead_code)] end: u64, } diff --git a/src/pbo.rs b/src/pbo.rs index d43fec9..9b232b2 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -12,6 +12,8 @@ use snafu::{ResultExt, Snafu}; pub struct Pbo { pub input: I, pub header_len: u64, + // We parse this but never really use it. + #[allow(dead_code)] pub extensions: HashMap, pub entries: Vec, } @@ -28,10 +30,14 @@ pub enum EntryType { pub struct PboEntry { pub filename: String, pub r#type: EntryType, + pub data_size: u32, + // We parse this but never really use it. + #[allow(dead_code)] pub original_size: u32, + #[allow(dead_code)] pub offset: u32, + #[allow(dead_code)] pub timestamp: u32, - pub data_size: u32, } #[derive(Debug, Snafu)] From cf2c7a754ee0e47fad3f1a536a6edc0f677d61bc Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 30 Dec 2024 23:47:19 -0300 Subject: [PATCH 29/34] launch: Guard a few things behind #[cfg(not(windows))]. --- src/commands/launch.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/launch.rs b/src/commands/launch.rs index bd7c853..153c578 100644 --- a/src/commands/launch.rs +++ b/src/commands/launch.rs @@ -1,14 +1,18 @@ use crate::mod_cache; use crate::mod_cache::ModCache; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; use std::cfg; use std::path::{Path, PathBuf}; +#[cfg(not(windows))] +use snafu::OptionExt; + #[derive(Debug, Snafu)] pub enum Error { #[snafu(display("failed to open ModCache: {}", source))] ModCacheOpen { source: mod_cache::Error }, #[snafu(display("failed to find drive_c"))] + #[cfg(not(windows))] FailedToFindDriveC, } From 7642843f4915b1271ae2791a768053c1ff20fe07 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 30 Dec 2024 23:09:42 -0300 Subject: [PATCH 30/34] ci: Deny warnings. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23b84c9..467c477 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ jobs: name: test env: RUST_BACKTRACE: 1 + RUSTFLAGS: "-Dwarnings" runs-on: ${{ matrix.os }} strategy: matrix: From 06572f25514746f65ed6010df6310762ee9fd0ef Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 30 Dec 2024 23:10:08 -0300 Subject: [PATCH 31/34] ci: Run Clippy. --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 467c477..451af60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,5 +27,8 @@ jobs: - name: Build run: cargo build --verbose + - name: Run Clippy + run: cargo clippy --verbose + - name: Run tests run: cargo test --verbose From 7d18b97113a073ad120384e675fb12c07679962b Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Mon, 30 Dec 2024 23:35:52 -0300 Subject: [PATCH 32/34] gen_srf, sync: Attempt to generate a mod cache if it doesn't exist. --- src/commands/gen_srf.rs | 16 +++++++++++++++- src/commands/launch.rs | 3 ++- src/commands/sync.rs | 4 ++-- src/mod_cache.rs | 13 +++++++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs index 1a46274..19333db 100644 --- a/src/commands/gen_srf.rs +++ b/src/commands/gen_srf.rs @@ -1,6 +1,6 @@ use crate::md5_digest::Md5Digest; use crate::mod_cache::ModCache; -use crate::srf; +use crate::{mod_cache, srf}; use rayon::prelude::*; use std::collections::HashMap; use std::fs::File; @@ -19,6 +19,20 @@ pub fn gen_srf_for_mod(mod_path: &Path) -> srf::Mod { generated_srf } +pub fn open_cache_or_gen_srf(base_path: &Path) -> Result { + match ModCache::from_disk(base_path) { + Ok(cache) => Ok(cache), + Err(mod_cache::Error::FileOpen { source }) + if source.kind() == std::io::ErrorKind::NotFound => + { + println!("nimble-cache.json not found, generating..."); + gen_srf(base_path); + ModCache::from_disk_or_empty(base_path) + } + Err(e) => Err(e), + } +} + pub fn gen_srf(base_path: &Path) { let mods: HashMap = WalkDir::new(base_path) .min_depth(1) diff --git a/src/commands/launch.rs b/src/commands/launch.rs index 153c578..63674e5 100644 --- a/src/commands/launch.rs +++ b/src/commands/launch.rs @@ -1,3 +1,4 @@ +use crate::commands::gen_srf::open_cache_or_gen_srf; use crate::mod_cache; use crate::mod_cache::ModCache; use snafu::{ResultExt, Snafu}; @@ -52,7 +53,7 @@ fn convert_host_base_path_to_proton_base_path(host_base_path: &Path) -> Result

Result<(), Error> { - let mod_cache = ModCache::from_disk_or_empty(base_path).context(ModCacheOpenSnafu)?; + let mod_cache = open_cache_or_gen_srf(base_path).context(ModCacheOpenSnafu)?; let proton_base_path = convert_host_base_path_to_proton_base_path(base_path)?; diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 197f1de..65804b8 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -1,4 +1,4 @@ -use crate::commands::gen_srf::gen_srf_for_mod; +use crate::commands::gen_srf::{gen_srf_for_mod, open_cache_or_gen_srf}; use crate::mod_cache::ModCache; use crate::{repository, srf}; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; @@ -243,7 +243,7 @@ pub fn sync( let remote_repo = repository::get_repository_info(agent, &format!("{repo_url}/repo.json")) .context(RepositoryFetchSnafu)?; - let mut mod_cache = ModCache::from_disk_or_empty(base_path).context(ModCacheOpenSnafu)?; + let mut mod_cache = open_cache_or_gen_srf(base_path).context(ModCacheOpenSnafu)?; let check = diff_repo(&mod_cache, &remote_repo); diff --git a/src/mod_cache.rs b/src/mod_cache.rs index f0ad8c3..ad8e529 100644 --- a/src/mod_cache.rs +++ b/src/mod_cache.rs @@ -52,7 +52,7 @@ impl ModCache { } } - pub fn from_disk_or_empty(repo_path: &Path) -> Result { + pub fn from_disk(repo_path: &Path) -> Result { let path = repo_path.join("nimble-cache.json"); let open_result = File::open(path); match open_result { @@ -60,11 +60,20 @@ impl ModCache { let reader = BufReader::new(file); serde_json::from_reader(reader).context(DeserializationSnafu) } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::new_empty()), Err(e) => Err(Error::FileOpen { source: e }), } } + pub fn from_disk_or_empty(repo_path: &Path) -> Result { + match Self::from_disk(repo_path) { + Ok(cache) => Ok(cache), + Err(Error::FileOpen { source }) if source.kind() == std::io::ErrorKind::NotFound => { + Ok(Self::new_empty()) + } + Err(e) => Err(e), + } + } + pub fn to_disk(&self, repo_path: &Path) -> Result<(), Error> { let path = repo_path.join("nimble-cache.json"); let file = File::create(path).context(FileCreationSnafu)?; From ee9169087df3fe0b9f114c438c321e453cfb635a Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Tue, 31 Dec 2024 00:41:26 -0300 Subject: [PATCH 33/34] README: Rewrite. * Remove goals and TODO list, will be moved over to issues. * Add install instructions. * Add usage instructions. --- README.md | 65 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b2dc8fa..58941ce 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,47 @@ # Nimble -Nimble is (or will be) a Swifty-compatible, cross platform, (currently) CLI only mod manager for Arma 3. -It is being built because I want to play with my Swifty using group on Linux :) - -## Goals -In order of priority: - * Full compatibility with Swifty - * Support all major platforms where you can reasonably run Arma 3 (Windows, Linux w/ Proton, maybe macOS?) - * Have decent usability - * This implies some form of user interface system. Rust's GUI story is kinda wonky, so maybe we'll have to settle for a TUI - * Generate Swifty repositories (swifty-cli create equivalent) - * Create symlinks instead of copying mod files - -## Big bucket list of things to do: - * Implement part level downloading, instead of redownloading entire files - * Clean up Path vs PathBuf vs &str vs String - * RelativePath and RelativePathBuf should be used in most cases. - * We still need to convert Windows backslashes to a sane separator on *nix platforms - * Use rayon for srf generation - * Somewhat done but needs improvement - * Properly deal with invalid PBOs \ No newline at end of file +Nimble is a Swifty-compatible, cross platform, (currently) CLI only mod manager for Arma 3. + +# Installing + +Nimble can be installed via Cargo: + +``` +cargo install --git https://github.com/vitorhnn/nimble.git +``` + +# Usage + +## Mod synchronization + +Unlike Swifty, Nimble (currently) is not capable of detecting when a repo is outdated, +so whenever your group pushes updates or when first installing, you must run: + +``` +nimble sync --repo-url --path +``` + +### Storage path restriction +For Linux under Proton, the mod storage path must be inside Arma 3's Proton prefix "drive_c", e.g: +``` +nimble sync --repo-url https://example.com/swifty/ --path /home/foo/.local/share/Steam/steamapps/compatdata/107410/pfx/drive_c/arma_mods +``` + +This restriction will be removed in the future. + +## Arma 3 launching + +On Windows and Linux with Proton, Nimble can launch Arma 3 using the `steam://` protocol: + +``` +nimble launch --path +``` + +## SRF generation + +The mod cache can be forcefully regenerated if required: +``` +nimble gen-srf --path +``` + +This should only be needed if you manually made changes to the mods. From 7cd40acccee2a87c3eb72cdfbd94cf5398cc7a09 Mon Sep 17 00:00:00 2001 From: Victor Chiletto Date: Tue, 31 Dec 2024 01:06:03 -0300 Subject: [PATCH 34/34] meta, ci: Introduce dist. Or, as the docs put it "wow shiny new dist CI!" :) --- .github/workflows/release.yml | 291 ++++++++++++++++++++++++++++++++++ Cargo.toml | 6 + dist-workspace.toml | 13 ++ 3 files changed, 310 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 dist-workspace.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1dffbcb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,291 @@ +# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ +# +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Release +permissions: + "contents": "write" + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However, GitHub +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + pull_request: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + +jobs: + # Run 'dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.27.0/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: host + shell: bash + run: | + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + + announce: + needs: + - plan + - host + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' }} + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive diff --git a/Cargo.toml b/Cargo.toml index 8811a89..671ead7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" authors = ["Victor Chiletto "] license = "GPL-3.0-or-later" +repository = "https://github.com/vitorhnn/nimble/" [dependencies] @@ -22,4 +23,9 @@ tempfile = "3" hex = "0.4" open = "3" percent-encoding = "2" + +# The profile that 'dist' will build with +[profile.dist] +inherits = "release" +lto = "thin" # tinyvec = { version = "1.5", features = ["alloc", "rustc_1_55"] } diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000..a9c05e2 --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,13 @@ +[workspace] +members = ["cargo:."] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.27.0" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = [] +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]