From cb51d16e0fc64b654235565e3cf3b50378f65faf Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Thu, 16 Oct 2025 16:49:46 +0300 Subject: [PATCH] initial commit --- .gitignore | 3 + Cargo.lock | 336 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 ++ src/main.rs | 53 ++++++++ src/utils.rs | 33 +++++ src/zdiff.rs | 127 +++++++++++++++++++ src/zpatch.rs | 24 ++++ 7 files changed, 589 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/utils.rs create mode 100644 src/zdiff.rs create mode 100644 src/zpatch.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae72c7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +target/ +test/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..557700c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,336 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "compression-core", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[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 = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zdiff" +version = "0.1.0" +dependencies = [ + "async-compression", + "md5", + "serde", + "serde_json", + "tokio", + "walkdir", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2a8504b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "zdiff" +version = "0.1.0" +edition = "2024" + +[dependencies] +zstd = { version = "0.13" } +async-compression = { version = "0.4", features = ["zstd"] } +tokio = { version = "1.48.0", features = ["rt", "rt-multi-thread", "macros"] } +md5 = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +walkdir = "2.5" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f09cbdf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,53 @@ +mod utils; +mod zdiff; +mod zpatch; + +use std::fs; +use std::fs::read; +use std::io; +use std::path::Path; + +async fn zdiff(filename: &str, old: &str, new: &str) -> Result<(), io::Error> { + let output_filename = &format!("{}.zdiff", filename); + let old_hashes = zdiff::walk_dir(old.to_string()).await; + let new_hashes = zdiff::walk_dir(new.to_string()).await; + let compare_hashes = zdiff::compare_hashes(old_hashes, new_hashes).await; + let parts = compare_hashes.to_vec().await; + utils::compress_parts(parts, fs::File::create(output_filename)?, 11).await; + Ok(()) +} + +async fn zpatch(filename: &str, dest_dir: &str) -> Result<(), io::Error> { + let filename = &format!("{}.zdiff", filename); + let parts = utils::decompress_parts(read(filename)?).await?; + let zdiff = zdiff::Zdiff::from_vec(parts).await?; + let tmp_dir_name = zpatch::extract_files(&zdiff, filename).await?; + for name in zdiff.content.keys().collect::>() { + let from_path = Path::new(&tmp_dir_name).join(name); + let to_path = Path::new(&dest_dir).join(name); + // println!("{:?} {:?}", from_path, to_path); + fs::create_dir_all(to_path.parent().unwrap())?; + fs::copy(from_path, to_path)?; + } + for file in zdiff.metadata.remove_files { + let path = Path::new(&dest_dir).join(file); + fs::remove_file(path)?; + } + + for (k, hash) in zdiff.metadata.hashes { + let path = Path::new(&dest_dir).join(k); + println!("path: {:?}", path); + let content = read(path)?; + let fs_hash = zdiff::get_hash(&content).await; + println!("{:?} {:?}", hash, fs_hash); + } + Ok(()) +} + +#[tokio::main] +async fn main() -> io::Result<()> { + let filename = "test"; + zdiff(filename, "test/old", "test/new").await?; + zpatch(filename, "old").await?; + Ok(()) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..bde2347 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,33 @@ +use std::{fs, io}; +use zstd::{Decoder, Encoder}; + +pub async fn compress_parts(input: Vec>, output: fs::File, level: i32) { + let mut encoder = Encoder::new(output, level).unwrap(); + for part in input.iter() { + io::copy(&mut &part[..], &mut encoder).unwrap(); + } + encoder.finish().unwrap(); +} + +pub async fn decompress_parts(input: Vec) -> Result>, io::Error> { + let mut decoder = Decoder::new(&input[..])?; + let mut buf = Vec::new(); + + io::copy(&mut decoder, &mut buf)?; + let mut index = 0; + let mut parts: Vec> = Vec::new(); + + while index < buf.len() { + let filename_size = u32::from_be_bytes(buf[index..index + 4].try_into().unwrap()) as usize; + let filename = buf[index..index + filename_size + 4].to_vec(); + index += 4 + filename_size; + + let content_size = u32::from_be_bytes(buf[index..index + 4].try_into().unwrap()) as usize; + let content = buf[index..index + content_size + 4].to_vec(); + index += content_size + 4; + + let part = vec![filename, content].concat(); + parts.push(part); + } + Ok(parts) +} diff --git a/src/zdiff.rs b/src/zdiff.rs new file mode 100644 index 0000000..2c6e94c --- /dev/null +++ b/src/zdiff.rs @@ -0,0 +1,127 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use walkdir::WalkDir; + +#[derive(Debug)] +pub struct Zdiff { + pub content: HashMap>, + pub metadata: Metadata, +} + +impl Zdiff { + pub async fn from_vec(_data: Vec>) -> Result { + let mut content = HashMap::new(); + for part in _data { + let filename_size = u32::from_be_bytes(part[0..4].try_into().unwrap()) as usize; + let filename = String::from_utf8(part[4..filename_size + 4].to_vec()).unwrap(); + let cont = part[filename_size + 8..].to_vec(); + content.insert(filename, cont); + } + let meta = content.get("metadata.json").unwrap(); + let metadata: Metadata = serde_json::from_slice(meta.as_slice())?; + content.remove("metadata.json"); + + Ok(Zdiff { content, metadata }) + } + + pub async fn to_vec(&self) -> Vec> { + let mut parts: Vec> = Vec::new(); + for (filename, content) in &self.content { + let filename_size: [u8; 4] = (filename.len() as u32).to_be_bytes(); + let filename_encoded = vec![filename_size.as_slice(), filename.as_bytes()].concat(); + + let content_size: [u8; 4] = (content.len() as u32).to_be_bytes(); + let content_encoded = vec![content_size.as_slice(), content.as_slice()].concat(); + parts.push(vec![filename_encoded, content_encoded].concat()) + } + + let meta = serde_json::to_vec(&self.metadata).unwrap(); + let meta_filename = "metadata.json"; + let meta_filename_size = (meta_filename.len() as u32).to_be_bytes(); + let meta_filename_encoded = + vec![meta_filename_size.as_slice(), meta_filename.as_bytes()].concat(); + + let meta_size = (meta.len() as u32).to_be_bytes(); + let meta_encoded = vec![meta_size.as_slice(), meta.as_slice()].concat(); + parts.push(vec![meta_filename_encoded, meta_encoded].concat()); + + parts + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Metadata { + diff_files: Vec, + pub hashes: HashMap, + pub remove_files: Vec, +} + +#[derive(Debug)] +pub struct FileInfo { + path: String, + relative_path: String, // Without dir prefix + hash: String, +} + +pub async fn get_hash(data: &Vec) -> String { + let hash = md5::compute(&data[..]); + format!("{:x}", hash) +} + +pub async fn walk_dir(dir: String) -> HashMap { + let mut hash_list: HashMap = HashMap::new(); + for e in WalkDir::new(&dir) { + let e = e.unwrap(); + let path = e.path(); + if path.is_dir() { + continue; + } + let content = fs::read(path).unwrap(); + let hash = get_hash(&content).await; + // let filename = path.file_name().unwrap().to_str().unwrap().to_string(); + let path_str = path.display().to_string(); + let file_info = FileInfo { + relative_path: path_str[dir.len() + 1..].to_string(), + path: path_str, + hash: hash.clone(), + }; + hash_list.insert(hash, file_info); + } + hash_list +} + +pub async fn compare_hashes( + old: HashMap, + new: HashMap, +) -> Zdiff { + let mut diff_files: HashMap> = HashMap::new(); + let mut remove_files: Vec = vec![]; + let mut hashes: HashMap = HashMap::new(); + for (_, info) in &old { + remove_files.push(info.relative_path.clone()); + } + + for (new_hash, new_fileinfo) in &new { + let old_fileinfo = old.get(new_hash); + remove_files.retain(|filename| !filename.eq(&new_fileinfo.relative_path)); + + if old_fileinfo.is_none() { + let path = new_fileinfo.relative_path.clone(); + diff_files.insert(path.clone(), fs::read(new_fileinfo.path.clone()).unwrap()); + hashes.insert( + new_fileinfo.relative_path.clone(), + new_fileinfo.hash.clone(), + ); + } + } + + Zdiff { + content: diff_files.clone(), + metadata: Metadata { + diff_files: diff_files.keys().cloned().collect(), + hashes, + remove_files, + }, + } +} diff --git a/src/zpatch.rs b/src/zpatch.rs new file mode 100644 index 0000000..0587f5e --- /dev/null +++ b/src/zpatch.rs @@ -0,0 +1,24 @@ +use crate::zdiff::Zdiff; +use crate::zpatch; +use std::fs; +use std::io::Write; +use std::path::Path; + +pub async fn create_tmp_dir(dir_name: String) -> Result { + let name = format!("{}.tmp", dir_name); + fs::remove_dir_all(name.clone()).map_err(|_| std::io::ErrorKind::NotFound)?; + fs::DirBuilder::new().create(name.clone())?; + Ok(name) +} + +pub async fn extract_files(zdiff: &Zdiff, filename: &String) -> Result { + let tmp_dir_name = create_tmp_dir(filename.to_string()).await?; + let path = Path::new(&tmp_dir_name); + fs::remove_dir_all(path)?; + for (f, c) in zdiff.content.iter() { + let filepath = path.join(f); + fs::create_dir_all(filepath.parent().unwrap())?; + fs::File::create(&filepath)?.write_all(c)?; + } + Ok(tmp_dir_name) +}