use std::{fs, path::PathBuf};
use hdiff_update_core::{
apply_patch, create_patch, prepare_delta_update, sha256_file, ApplyPatchOptions, Artifact,
CreatePatchOptions, DeltaArtifact, PlatformRelease, PrepareUpdateOptions, PreparedUpdateKind,
UpdateManifest, DEFAULT_ALGORITHM,
};
use tempfile::tempdir;
#[test]
fn hdiff_tools_roundtrip_small_binary() {
let workspace = workspace_root();
let hdiffz = workspace.join("hdiffz.exe");
let hpatchz = workspace.join("hpatchz.exe");
if !hdiffz.is_file() || !hpatchz.is_file() {
eprintln!("skipping HDiffPatch roundtrip because hdiffz.exe or hpatchz.exe is missing");
return;
}
let dir = tempdir().expect("tempdir");
let old = dir.path().join("old.bin");
let new = dir.path().join("new.bin");
let patch = dir.path().join("delta.hpatch");
let output = dir.path().join("output.bin");
let mut old_bytes = vec![0_u8; 1024 * 1024];
let mut new_bytes = vec![0_u8; 1024 * 1024];
for index in 0..old_bytes.len() {
old_bytes[index] = (index % 251) as u8;
new_bytes[index] = old_bytes[index];
}
for index in 100_000..105_000 {
new_bytes[index] = ((index * 7) % 251) as u8;
}
fs::write(&old, old_bytes).expect("old");
fs::write(&new, new_bytes).expect("new");
let created = create_patch(&CreatePatchOptions {
old_path: old.clone(),
new_path: new.clone(),
patch_path: patch.clone(),
hdiffz_path: Some(hdiffz),
force: true,
step_size: Some("64k".to_string()),
old_window_size: Some("512k".to_string()),
compression: Some("zstd-20".to_string()),
checksum: Some("xxh128".to_string()),
parallel_threads: Some(2),
})
.expect("create patch");
assert!(created.patch.size > 0);
let expected = sha256_file(&new).expect("hash new").sha256;
let applied = apply_patch(&ApplyPatchOptions {
old_path: old,
patch_path: patch,
output_path: output,
hpatchz_path: Some(hpatchz),
force: true,
expected_sha256: Some(expected.clone()),
cache_size: Some("8m".to_string()),
parallel_threads: Some(2),
verify_checksums: true,
})
.expect("apply patch");
assert_eq!(applied.output.sha256, expected);
assert_eq!(
fs::read(new).expect("read new"),
fs::read(applied.output.path).expect("read out")
);
}
#[tokio::test]
async fn prepare_delta_update_applies_matching_cached_installer() {
let workspace = workspace_root();
let hdiffz = workspace.join("hdiffz.exe");
let hpatchz = workspace.join("hpatchz.exe");
if !hdiffz.is_file() || !hpatchz.is_file() {
eprintln!(
"skipping prepare_delta_update roundtrip because hdiffz.exe or hpatchz.exe is missing"
);
return;
}
let dir = tempdir().expect("tempdir");
let server = dir.path().join("server");
let cache = dir.path().join("cache");
fs::create_dir_all(&server).expect("server dir");
let old = dir.path().join("xCodex_0.1.0_x64-setup.exe");
let new = server.join("xCodex_0.2.0_x64-setup.exe");
let patch = server.join("0.1.0_to_0.2.0.hpatch");
let output = cache.join("xCodex_0.2.0_x64-setup.exe");
let old_bytes = deterministic_bytes(1024 * 1024, 3);
let mut new_bytes = old_bytes.clone();
for index in 200_000..210_000 {
new_bytes[index] = ((index * 17) % 251) as u8;
}
fs::write(&old, old_bytes).expect("old");
fs::write(&new, new_bytes).expect("new");
let patch_result = create_patch(&CreatePatchOptions {
old_path: old.clone(),
new_path: new.clone(),
patch_path: patch.clone(),
hdiffz_path: Some(hdiffz),
force: true,
step_size: Some("64k".to_string()),
old_window_size: Some("512k".to_string()),
compression: Some("zstd-20".to_string()),
checksum: Some("xxh128".to_string()),
parallel_threads: Some(2),
})
.expect("create patch");
let new_digest = sha256_file(&new).expect("new digest");
let manifest_path = server.join("latest.json");
fs::write(
&manifest_path,
serde_json::to_vec_pretty(&manifest(
&patch_result.old.sha256,
&patch_result.patch.sha256,
patch_result.patch.size,
&new_digest.sha256,
new_digest.size,
))
.expect("manifest json"),
)
.expect("write manifest");
let prepared = prepare_delta_update(
PrepareUpdateOptions {
manifest_url: manifest_path.to_string_lossy().to_string(),
platform: Some("windows-x86_64".to_string()),
current_version: Some("0.1.0".to_string()),
expected_version: Some("0.2.0".to_string()),
current_artifact_path: Some(old),
output_path: Some(output.clone()),
patch_path: Some(cache.join("patches").join("delta.hpatch")),
cache_dir: Some(cache),
hpatchz_path: Some(hpatchz),
signature_public_key: None,
require_signature: false,
force: true,
headers: Vec::new(),
timeout_secs: None,
},
|_| {},
)
.await
.expect("prepare delta");
assert!(matches!(prepared.kind, PreparedUpdateKind::Delta));
assert_eq!(prepared.artifact.sha256, new_digest.sha256);
assert_eq!(
fs::read(new).expect("read new"),
fs::read(output).expect("read output")
);
}
#[tokio::test]
async fn prepare_delta_update_does_not_fall_back_to_full_download() {
let dir = tempdir().expect("tempdir");
let server = dir.path().join("server");
fs::create_dir_all(&server).expect("server dir");
let current = dir.path().join("current-installer.exe");
let output = dir.path().join("should-not-exist.exe");
fs::write(¤t, b"not the declared source installer").expect("current");
fs::write(server.join("xCodex_0.2.0_x64-setup.exe"), b"full installer").expect("full");
let manifest_path = server.join("latest.json");
fs::write(
&manifest_path,
serde_json::to_vec_pretty(&manifest(
"0000000000000000000000000000000000000000000000000000000000000000",
"1111111111111111111111111111111111111111111111111111111111111111",
1,
"2222222222222222222222222222222222222222222222222222222222222222",
14,
))
.expect("manifest json"),
)
.expect("write manifest");
let error = prepare_delta_update(
PrepareUpdateOptions {
manifest_url: manifest_path.to_string_lossy().to_string(),
platform: Some("windows-x86_64".to_string()),
current_version: Some("0.1.0".to_string()),
expected_version: Some("0.2.0".to_string()),
current_artifact_path: Some(current),
output_path: Some(output.clone()),
patch_path: Some(dir.path().join("delta.hpatch")),
cache_dir: Some(dir.path().join("cache")),
hpatchz_path: None,
signature_public_key: None,
require_signature: false,
force: true,
headers: Vec::new(),
timeout_secs: None,
},
|_| {},
)
.await
.expect_err("delta-only prepare should reject missing source hash");
assert!(
matches!(error, hdiff_update_core::Error::NoMatchingArtifact { .. }),
"{error:?}"
);
assert!(!output.exists());
}
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|path| path.parent())
.expect("workspace root")
.to_path_buf()
}
fn deterministic_bytes(size: usize, salt: u8) -> Vec<u8> {
(0..size)
.map(|index| ((index * 31 + salt as usize) % 251) as u8)
.collect()
}
fn manifest(
old_sha256: &str,
patch_sha256: &str,
patch_size: u64,
new_sha256: &str,
new_size: u64,
) -> UpdateManifest {
UpdateManifest {
version: "0.2.0".to_string(),
notes: None,
published_at: None,
signature: None,
platforms: [(
"windows-x86_64".to_string(),
PlatformRelease {
full: Artifact {
url: "xCodex_0.2.0_x64-setup.exe".to_string(),
sha256: new_sha256.to_string(),
size: new_size,
signature: None,
},
deltas: vec![DeltaArtifact {
from_version: Some("0.1.0".to_string()),
from_sha256: old_sha256.to_string(),
url: "0.1.0_to_0.2.0.hpatch".to_string(),
sha256: patch_sha256.to_string(),
size: patch_size,
target_sha256: Some(new_sha256.to_string()),
target_size: Some(new_size),
algorithm: DEFAULT_ALGORITHM.to_string(),
}],
},
)]
.into(),
}
}