From d71acd5a20fb4c695375000776f9feddd08a839a Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Tue, 13 May 2025 15:57:03 +0100 Subject: [PATCH] First cut of a rust delta implementation --- Cargo.lock | 213 +++++++++++++++++++++++++++++++-- Cargo.toml | 5 +- src/bins/Cargo.toml | 8 +- src/bins/src/commands/mod.rs | 3 + src/bins/src/commands/patch.rs | 206 +++++++++++++++++++++++++++++++ src/bins/src/update.rs | 31 +++-- src/bins/tests/commands.rs | 42 ++++++- src/lib-rust/Cargo.toml | 8 +- src/lib-rust/src/delta.rs | 48 -------- src/lib-rust/src/lib.rs | 3 - src/lib-rust/src/logging.rs | 15 ++- 11 files changed, 494 insertions(+), 88 deletions(-) create mode 100644 src/bins/src/commands/patch.rs delete mode 100644 src/lib-rust/src/delta.rs diff --git a/Cargo.lock b/Cargo.lock index 16f6eb3f..6ce860fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -310,6 +321,25 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cbindgen" version = "0.28.0" @@ -366,6 +396,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.38" @@ -427,6 +467,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation" version = "0.10.0" @@ -452,6 +498,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -521,6 +582,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "deranged" version = "0.4.1" @@ -576,6 +643,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -634,7 +702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -796,8 +864,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -853,6 +923,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.2.0" @@ -1064,6 +1143,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1133,7 +1221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1200,6 +1288,27 @@ dependencies = [ "log", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1351,6 +1460,16 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1382,9 +1501,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" @@ -1595,7 +1714,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1608,7 +1727,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.2", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1841,6 +1960,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1901,7 +2029,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.0.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2232,7 +2360,6 @@ dependencies = [ "windows", "xml", "zip", - "zstd", ] [[package]] @@ -2273,15 +2400,20 @@ dependencies = [ "simplelog", "strsim 0.11.1", "strum", + "substring", "tempfile", "time 0.3.41", "velopack", "wait-timeout", "waitpid-any", + "walkdir", "webview2-com-sys", "windows", "winres", "winsafe", + "zip", + "zip-extensions", + "zstd", ] [[package]] @@ -2338,6 +2470,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -2500,7 +2642,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -2827,6 +2969,15 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yansi" version = "1.0.1" @@ -2924,6 +3075,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] [[package]] name = "zerovec" @@ -2953,13 +3118,35 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" dependencies = [ + "aes", "arbitrary", + "bzip2", + "constant_time_eq", "crc32fast", "crossbeam-utils", + "deflate64", "flate2", + "getrandom 0.3.1", + "hmac", "indexmap", + "lzma-rs", "memchr", + "pbkdf2", + "sha1", + "time 0.3.41", + "xz2", + "zeroize", "zopfli", + "zstd", +] + +[[package]] +name = "zip-extensions" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cdbf826e5a6eec81fc5a0d33cd7c09c31fd8f9918f15434f74c42d39ef337a" +dependencies = [ + "zip", ] [[package]] @@ -2987,18 +3174,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.3" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 6af625bb..69113a48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,8 @@ derivative = "2.2" glob = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } -zip = { version = "2.2", default-features = false, features = ["deflate"] } +zip = { version = "2.6", default-features = false, features = ["deflate"] } +zip-extensions = "0.8.2" thiserror = "2.0" lazy_static = "1.5" regex = "1.10" @@ -83,6 +84,8 @@ log-panics = "2.1.0" core-foundation = "0.10" core-foundation-sys = "0.8" uuid = { version = "1.13.1", features = ["v4", "fast-rng", "macro-diagnostics"] } +walkdir = "2.5" +substring = " 1.4" # default to small, optimized workspace release binaries [profile.release] diff --git a/src/bins/Cargo.toml b/src/bins/Cargo.toml index 780c533f..76427c28 100644 --- a/src/bins/Cargo.toml +++ b/src/bins/Cargo.toml @@ -63,6 +63,12 @@ wait-timeout.workspace = true pretty-bytes-rust.workspace = true enum-flags.workspace = true log-panics.workspace = true +zstd.workspace = true +zip.workspace = true +zip-extensions.workspace = true +walkdir.workspace = true +sha1_smol.workspace = true +substring.workspace = true [target.'cfg(target_os="linux")'.dependencies] waitpid-any.workspace = true @@ -101,6 +107,7 @@ windows = { workspace = true, features = [ "Win32_UI_Shell_Common", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging", + "Win32_System_ApplicationInstallationAndServicing", "Win32_System_Kernel", "Wdk", "Wdk_System", @@ -116,7 +123,6 @@ same-file.workspace = true tempfile.workspace = true ntest.workspace = true pretty_assertions.workspace = true -sha1_smol.workspace = true [build-dependencies] semver.workspace = true diff --git a/src/bins/src/commands/mod.rs b/src/bins/src/commands/mod.rs index e6de8bfe..29d9b652 100644 --- a/src/bins/src/commands/mod.rs +++ b/src/bins/src/commands/mod.rs @@ -4,6 +4,9 @@ pub use apply::*; mod start; pub use start::*; +mod patch; +pub use patch::*; + #[cfg(target_os = "linux")] mod apply_linux_impl; #[cfg(target_os = "macos")] diff --git a/src/bins/src/commands/patch.rs b/src/bins/src/commands/patch.rs new file mode 100644 index 00000000..c5d1acbe --- /dev/null +++ b/src/bins/src/commands/patch.rs @@ -0,0 +1,206 @@ +use anyhow::{anyhow, bail, Result}; +use std::os::windows::fs::MetadataExt; +use std::{ + collections::HashSet, + fs, io, + path::{Path, PathBuf}, +}; +use walkdir::WalkDir; +use zip::{write::SimpleFileOptions, CompressionMethod}; +use zip_extensions::{zip_create_from_directory_with_options, zip_extract}; + +pub fn zstd_patch_single, P2: AsRef, P3: AsRef>(old_file: P1, patch_file: P2, output_file: P3) -> Result<()> { + let old_file = old_file.as_ref(); + let patch_file = patch_file.as_ref(); + let output_file = output_file.as_ref(); + + if !old_file.exists() { + bail!("Old file does not exist: {}", old_file.to_string_lossy()); + } + + if !patch_file.exists() { + bail!("Patch file does not exist: {}", patch_file.to_string_lossy()); + } + + let dict = fs::read(old_file)?; + + // info!("Loading Dictionary (Size: {})", dict.len()); + let patch = fs::OpenOptions::new().read(true).open(patch_file)?; + let patch_reader = io::BufReader::new(patch); + let mut decoder = zstd::Decoder::with_dictionary(patch_reader, &dict)?; + + let window_log = fio_highbit64(dict.len() as u64) + 1; + if window_log >= 27 { + info!("Large File detected. Overriding windowLog to {}", window_log); + decoder.window_log_max(window_log)?; + } + + // info!("Decoder loaded. Beginning patch..."); + let mut output = fs::OpenOptions::new().write(true).create(true).truncate(true).open(output_file)?; + io::copy(&mut decoder, &mut output)?; + + // info!("Patch applied successfully."); + Ok(()) +} + +fn fio_highbit64(v: u64) -> u32 { + let mut count: u32 = 0; + let mut v = v; + v >>= 1; + while v > 0 { + v >>= 1; + count += 1; + } + return count; +} + +pub fn delta, P2: AsRef, P3: AsRef>( + old_file: P1, + delta_files: Vec<&PathBuf>, + temp_dir: P2, + output_file: P3, +) -> Result<()> { + let old_file = old_file.as_ref().to_path_buf(); + let temp_dir = temp_dir.as_ref().to_path_buf(); + let output_file = output_file.as_ref().to_path_buf(); + + if !old_file.exists() { + bail!("Old file does not exist: {}", old_file.to_string_lossy()); + } + + if delta_files.is_empty() { + bail!("No delta files provided."); + } + + for delta_file in &delta_files { + if !delta_file.exists() { + bail!("Delta file does not exist: {}", delta_file.to_string_lossy()); + } + } + + let time = simple_stopwatch::Stopwatch::start_new(); + + info!("Extracting base package for delta patching: {}", temp_dir.to_string_lossy()); + let work_dir = temp_dir.join("_work"); + fs::create_dir_all(&work_dir)?; + zip_extract(&old_file, &work_dir)?; + + info!("Base package extracted. {} delta packages to apply.", delta_files.len()); + + for (i, delta_file) in delta_files.iter().enumerate() { + info!("{}: extracting apply delta patch: {}", i, delta_file.to_string_lossy()); + let delta_dir = temp_dir.join(format!("delta_{}", i)); + fs::create_dir_all(&delta_dir)?; + zip_extract(delta_file, &delta_dir)?; + + let delta_relative_paths: Vec = WalkDir::new(&delta_dir) + .follow_links(false) + .into_iter() + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().is_file()) + .map(|entry| entry.path().strip_prefix(&delta_dir).map(|p| p.to_path_buf())) + .filter_map(|entry| entry.ok()) + .collect(); + + let mut visited_paths = HashSet::new(); + + // apply all the zsdiff patches for files which exist in both the delta and the base package + for relative_path in &delta_relative_paths { + if relative_path.starts_with("lib") { + let file_name = relative_path.file_name().ok_or(anyhow!("Failed to get file name"))?; + let file_name_str = file_name.to_string_lossy(); + if file_name_str.ends_with(".zsdiff") || file_name_str.ends_with(".diff") || file_name_str.ends_with(".bsdiff") { + // this is a zsdiff patch, we need to apply it to the old file + let file_without_extension = relative_path.with_extension(""); + // let shasum_path = delta_dir.join(relative_path).with_extension("shasum"); + let old_file_path = work_dir.join(&file_without_extension); + let patch_file_path = delta_dir.join(&relative_path); + let output_file_path = delta_dir.join(&file_without_extension); + + visited_paths.insert(file_without_extension); + + if fs::metadata(&patch_file_path)?.file_size() == 0 { + // file has not changed, so we can continue. + continue; + } + + if file_name_str.ends_with(".zsdiff") { + info!("{}: applying zsdiff patch: {:?}", i, relative_path); + zstd_patch_single(&old_file_path, &patch_file_path, &output_file_path)?; + } else { + bail!("Unsupported patch format: {:?}", relative_path); + } + + fs::rename(&output_file_path, &old_file_path)?; + } else if file_name_str.ends_with(".shasum") { + // skip shasum files + } else { + // if this file is inside the lib folder without a known extension, it is a new file + let file_path = delta_dir.join(relative_path); + let dest_path = work_dir.join(relative_path); + info!("{}: new file: {:?}", i, relative_path); + fs::copy(&file_path, &dest_path)?; + visited_paths.insert(relative_path.clone()); + } + } else { + // if this file is not inside the lib folder, we always copy it over + let file_path = delta_dir.join(relative_path); + let dest_path = work_dir.join(relative_path); + info!("{}: copying metadata file: {:?}", i, relative_path); + fs::copy(&file_path, &dest_path)?; + visited_paths.insert(relative_path.clone()); + } + } + + // anything in the work dir which was not visited is an old / deleted file and should be removed + let workdir_relative_paths: Vec = WalkDir::new(&work_dir) + .follow_links(false) + .into_iter() + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().is_file()) + .map(|entry| entry.path().strip_prefix(&work_dir).map(|p| p.to_path_buf())) + .filter_map(|entry| entry.ok()) + .collect(); + + for relative_path in &workdir_relative_paths { + if !visited_paths.contains(relative_path) { + let file_to_delete = work_dir.join(relative_path); + info!("{}: deleting old/removed file: {:?}", i, relative_path); + let _ = fs::remove_file(file_to_delete); // soft error + } + } + } + + info!("All delta patches applied. Asembling output package at: {}", output_file.to_string_lossy()); + + // NOTE: zstd is not supported by older versions of Squirrel/Velopack, but + // it's assumed if someone is using this code, they are using a recent enough version + let options = SimpleFileOptions::default().compression_method(CompressionMethod::Zstd); + zip_create_from_directory_with_options(&output_file, &work_dir, |_| options)?; + + info!("Successfully applied {} delta patches in {}s.", delta_files.len(), time.s()); + Ok(()) +} + +// NOTE: this is some code to do checksum verification, but it is not being used +// by the current implementation because zstd patching already has checksum verification +// +// let actual_checksum = get_sha1(&output_file_path); +// let expected_checksum = load_release_entry_shasum(&shasum_path)?; +// +// if !actual_checksum.eq_ignore_ascii_case(&expected_checksum) { +// bail!("Checksum mismatch for: {:?}. Expected: {}, Actual: {}", relative_path, expected_checksum, actual_checksum); +// } +// fn load_release_entry_shasum(file: &PathBuf) -> Result { +// let raw_text = fs::read_to_string(file)?.trim().to_string(); +// let first_word = raw_text.splitn(2, ' ').next().unwrap(); +// let cleaned = first_word.trim().trim_matches(|c: char| !c.is_ascii_hexdigit()); +// Ok(cleaned.to_string()) +// } +// +// fn get_sha1(file: &PathBuf) -> String { +// let file_bytes = fs::read(file).unwrap(); +// let mut sha1 = sha1_smol::Sha1::new(); +// sha1.update(&file_bytes); +// sha1.digest().to_string() +// } diff --git a/src/bins/src/update.rs b/src/bins/src/update.rs index 90706722..80563b74 100644 --- a/src/bins/src/update.rs +++ b/src/bins/src/update.rs @@ -5,7 +5,7 @@ extern crate log; use anyhow::{anyhow, bail, Result}; -use clap::{arg, value_parser, ArgMatches, Command}; +use clap::{arg, value_parser, ArgAction, ArgMatches, Command}; use std::{env, path::PathBuf}; use velopack::locator::{auto_locate_app_manifest, LocationContext}; use velopack::logging::*; @@ -34,9 +34,9 @@ fn root_command() -> Command { .long_flag_aliases(vec!["processStart", "processStartAndWait"]) ) .subcommand(Command::new("patch") - .about("Applies a Zstd patch file") + .about("Applies a series of delta bundles to a base file") .arg(arg!(--old "Base / old file to apply the patch to").required(true).value_parser(value_parser!(PathBuf))) - .arg(arg!(--patch "The Zstd patch to apply to the old file").required(true).value_parser(value_parser!(PathBuf))) + .arg(arg!(--delta "The delta bundle to apply to the base package").required(true).action(ArgAction::Append).value_parser(value_parser!(PathBuf))) .arg(arg!(--output "The file to create with the patch applied").required(true).value_parser(value_parser!(PathBuf))) ) .arg(arg!(--verbose "Print debug messages to console / log").global(true)) @@ -180,19 +180,34 @@ fn main() -> Result<()> { fn patch(matches: &ArgMatches) -> Result<()> { let old_file = matches.get_one::("old"); - let patch_file = matches.get_one::("patch"); + let deltas: Vec<&PathBuf> = matches.get_many::("delta").unwrap_or_default().collect(); let output_file = matches.get_one::("output"); info!("Command: Patch"); info!(" Old File: {:?}", old_file); - info!(" Patch File: {:?}", patch_file); + info!(" Delta Files: {:?}", deltas); info!(" Output File: {:?}", output_file); - if old_file.is_none() || patch_file.is_none() || output_file.is_none() { - bail!("Missing required arguments. Please provide --old, --patch, and --output."); + if old_file.is_none() || deltas.is_empty() || output_file.is_none() { + bail!("Missing required arguments. Please provide --old, --delta, and --output."); } - velopack::delta::zstd_patch_single(old_file.unwrap(), patch_file.unwrap(), output_file.unwrap())?; + let temp_dir = match auto_locate_app_manifest(LocationContext::IAmUpdateExe) { + Ok(locator) => locator.get_temp_dir_rand16(), + Err(_) => { + let mut temp_dir = std::env::temp_dir(); + let rand = shared::random_string(16); + temp_dir.push("velopack_".to_owned() + &rand); + temp_dir + } + }; + + let result = commands::delta(old_file.unwrap(), deltas, &temp_dir, output_file.unwrap()); + let _ = remove_dir_all::remove_dir_all(temp_dir); + + if let Err(e) = result { + bail!("Delta error: {}", e); + } Ok(()) } diff --git a/src/bins/tests/commands.rs b/src/bins/tests/commands.rs index 32a8bb79..f6defce8 100644 --- a/src/bins/tests/commands.rs +++ b/src/bins/tests/commands.rs @@ -3,13 +3,14 @@ mod common; use common::*; use std::{fs, path::Path, path::PathBuf}; +use std::hint::assert_unchecked; use tempfile::tempdir; use velopack_bins::*; -#[cfg(target_os = "windows")] -use winsafe::{self as w, co}; use velopack::bundle::load_bundle_from_file; use velopack::locator::{auto_locate_app_manifest, LocationContext}; +#[cfg(target_os = "windows")] +use winsafe::{self as w, co}; #[cfg(target_os = "windows")] #[test] @@ -87,7 +88,7 @@ pub fn test_install_preserve_symlinks() { let tmp_dir = tempdir().unwrap(); let tmp_buf = tmp_dir.path().to_path_buf(); let mut tmp_zip = load_bundle_from_file(nupkg).unwrap(); - + commands::install(&mut tmp_zip, Some(&tmp_buf), None).unwrap(); assert!(tmp_buf.join("current").join("actual").join("file.txt").exists()); @@ -119,13 +120,42 @@ pub fn test_patch_apply() { let expected_sha1 = get_sha1(&new_file); let tmp_file = Path::new("temp.patch").to_path_buf(); - velopack::delta::zstd_patch_single(&old_file, &p1, &tmp_file).unwrap(); + velopack_bins::commands::zstd_patch_single(&old_file, &p1, &tmp_file).unwrap(); let tmp_sha1 = get_sha1(&tmp_file); fs::remove_file(&tmp_file).unwrap(); assert_eq!(expected_sha1, tmp_sha1); - velopack::delta::zstd_patch_single(&old_file, &p2, &tmp_file).unwrap(); + velopack_bins::commands::zstd_patch_single(&old_file, &p2, &tmp_file).unwrap(); let tmp_sha1 = get_sha1(&tmp_file); fs::remove_file(&tmp_file).unwrap(); assert_eq!(expected_sha1, tmp_sha1); -} +} + +#[test] +pub fn test_delta_apply_legacy() { + dialogs::set_silent(true); + let fixtures = find_fixtures(); + let base = fixtures.join("Clowd-3.4.287-full.nupkg"); + let d1 = fixtures.join("Clowd-3.4.288-delta-zstd.nupkg"); + let d2 = fixtures.join("Clowd-3.4.291-delta-zstd.nupkg"); + let d3 = fixtures.join("Clowd-3.4.292-delta-zstd.nupkg"); + let d4 = fixtures.join("Clowd-3.4.293-delta-zstd.nupkg"); + + let deltas = vec![&d1, &d2, &d3, &d4]; + + let tmp_dir = tempdir().unwrap(); + let temp_output = tmp_dir.path().join("Clowd-3.4.293-full.nupkg"); + commands::delta(&base, deltas, tmp_dir.path(), &temp_output).unwrap(); + + let mut bundle = load_bundle_from_file(temp_output).unwrap(); + let manifest = bundle.read_manifest().unwrap(); + + assert_eq!(manifest.id, "Clowd"); + assert_eq!(manifest.version, semver::Version::parse("3.4.293").unwrap()); + + let extract_dir = tmp_dir.path().join("_extracted"); + bundle.extract_lib_contents_to_path(&extract_dir, |_| {}).unwrap(); + + let extracted = extract_dir.join("Clowd.dll"); + assert!(extracted.exists()); +} diff --git a/src/lib-rust/Cargo.toml b/src/lib-rust/Cargo.toml index 158f1752..f522432e 100644 --- a/src/lib-rust/Cargo.toml +++ b/src/lib-rust/Cargo.toml @@ -14,14 +14,13 @@ edition.workspace = true rust-version.workspace = true [features] -default = ["zstd"] -delta = ["zstd"] +default = [] async = ["async-std"] typescript = ["ts-rs"] file-logging = ["log-panics", "simplelog", "file-rotate", "time"] [package.metadata.docs.rs] -features = ["async", "delta"] +features = ["async"] [lib] name = "velopack" @@ -54,9 +53,6 @@ uuid.workspace = true # typescript ts-rs = { workspace = true, optional = true } -# delta packages -zstd = { workspace = true, optional = true } - # async async-std = { workspace = true, optional = true } diff --git a/src/lib-rust/src/delta.rs b/src/lib-rust/src/delta.rs deleted file mode 100644 index 40ea7222..00000000 --- a/src/lib-rust/src/delta.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::{fs, io, path::Path}; -use crate::Error; - -/// Applies a zstd patch to a single file by loading the patch as a dictionary. -pub fn zstd_patch_single, P2: AsRef, P3: AsRef>(old_file: P1, patch_file: P2, output_file: P3) -> Result<(), Error> { - let old_file = old_file.as_ref(); - let patch_file = patch_file.as_ref(); - let output_file = output_file.as_ref(); - - if !old_file.exists() { - return Err(Error::FileNotFound(old_file.to_string_lossy().to_string())); - } - - if !patch_file.exists() { - return Err(Error::FileNotFound(patch_file.to_string_lossy().to_string())); - } - - let dict = fs::read(old_file)?; - - info!("Loading Dictionary (Size: {})", dict.len()); - let patch = fs::OpenOptions::new().read(true).open(patch_file)?; - let patch_reader = io::BufReader::new(patch); - let mut decoder = zstd::Decoder::with_dictionary(patch_reader, &dict)?; - - let window_log = fio_highbit64(dict.len() as u64) + 1; - if window_log >= 27 { - info!("Large File detected. Overriding windowLog to {}", window_log); - decoder.window_log_max(window_log)?; - } - - info!("Decoder loaded. Beginning patch..."); - let mut output = fs::OpenOptions::new().write(true).create(true).truncate(true).open(output_file)?; - io::copy(&mut decoder, &mut output)?; - - info!("Patch applied successfully."); - Ok(()) -} - -fn fio_highbit64(v: u64) -> u32 { - let mut count: u32 = 0; - let mut v = v; - v >>= 1; - while v > 0 { - v >>= 1; - count += 1; - } - return count; -} \ No newline at end of file diff --git a/src/lib-rust/src/lib.rs b/src/lib-rust/src/lib.rs index e2bf7580..dfe36739 100644 --- a/src/lib-rust/src/lib.rs +++ b/src/lib-rust/src/lib.rs @@ -97,9 +97,6 @@ pub mod locator; /// Sources contains abstractions for custom update sources (eg. url, local file, github releases, etc). pub mod sources; -/// Functions to patch files and reconstruct Velopack delta packages. -pub mod delta; - /// Acquire and manage file-system based lock files. pub mod lockfile; diff --git a/src/lib-rust/src/logging.rs b/src/lib-rust/src/logging.rs index 4e208c8c..ddebcf41 100644 --- a/src/lib-rust/src/logging.rs +++ b/src/lib-rust/src/logging.rs @@ -76,12 +76,18 @@ pub fn default_logfile_path>(locator: L) -> PathBuf /// It can only be called once per process, and should be called early in the process lifecycle. /// Future calls to this function will fail. #[cfg(feature = "file-logging")] -pub fn init_logging(process_name: &str, file: Option<&PathBuf>, console: bool, verbose: bool, custom_log_cb: Option>) { +pub fn init_logging( + process_name: &str, + file: Option<&PathBuf>, + console: bool, + verbose: bool, + custom_log_cb: Option>, +) { let mut loggers: Vec> = Vec::new(); if let Some(cb) = custom_log_cb { loggers.push(cb); } - + let color_choice = ColorChoice::Never; if console { let console_level = if verbose { LevelFilter::Debug } else { LevelFilter::Info }; @@ -105,6 +111,11 @@ pub fn init_logging(process_name: &str, file: Option<&PathBuf>, console: bool, v } } +/// Initialize a Trace / Console logger for the current process. +pub fn trace_logger() { + TermLogger::init(LevelFilter::Trace, get_config(None), TerminalMode::Mixed, ColorChoice::Never).unwrap(); +} + #[cfg(feature = "file-logging")] fn get_config(process_name: Option<&str>) -> Config { let mut c = ConfigBuilder::default();