use std::{collections::BTreeMap, fs, path::PathBuf};
use hdiff_update_core::{
apply_directory_patch, build_file_tree_manifest, create_directory_patch,
prepare_directory_update, ApplyDirectoryPatchOptions, Artifact, CreateDirectoryPatchOptions,
DirectoryDelta, DirectoryPlatformRelease, DirectoryUpdateManifest,
PrepareDirectoryUpdateOptions, PreparedDirectoryUpdateKind, DIRECTORY_UPDATE_ALGORITHM,
DIRECTORY_UPDATE_SCHEMA_VERSION,
};
use tempfile::tempdir;
#[test]
fn directory_patch_roundtrip_ignores_unmanaged_installer_files() {
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 tools are missing");
return;
}
let dir = tempdir().expect("tempdir");
let old = dir.path().join("old");
let new = dir.path().join("new");
let replay = dir.path().join("replay");
let patch = dir.path().join("payload.hdiff");
let managed = vec![
"app.exe".to_string(),
"plugins".to_string(),
"resources".to_string(),
];
create_payload(&old, b"old-main", b"old-resource");
create_payload(&new, b"new-main", b"new-resource");
fs::write(old.join("uninstall.exe"), b"unmanaged").expect("unmanaged old file");
let target = build_file_tree_manifest(&new, &managed).expect("target manifest");
let created = create_directory_patch(&CreateDirectoryPatchOptions {
old_path: old.clone(),
new_path: new,
patch_path: patch.clone(),
managed_paths: managed.clone(),
hdiffz_path: Some(hdiffz),
force: true,
step_size: Some("64k".to_string()),
compression: Some("lzma2-9-8m".to_string()),
checksum: Some("xxh128".to_string()),
parallel_threads: Some(2),
})
.expect("create directory patch");
assert!(created.patch.size > 0);
let applied = apply_directory_patch(&ApplyDirectoryPatchOptions {
old_path: old,
patch_path: patch,
output_path: replay,
managed_paths: managed,
expected_tree: target.clone(),
hpatchz_path: Some(hpatchz),
cache_size: Some("8m".to_string()),
parallel_threads: Some(2),
verify_checksums: true,
})
.expect("apply directory patch");
assert_eq!(applied.output.tree_sha256, target.tree_sha256);
}
#[tokio::test]
async fn preparation_reuses_a_verified_ready_transaction() {
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 preparation because tools are missing");
return;
}
let dir = tempdir().expect("tempdir");
let old = dir.path().join("installed");
let new = dir.path().join("new");
let server = dir.path().join("server");
let cache = dir.path().join("cache");
fs::create_dir_all(&server).expect("server");
let patch = server.join("payload.hdiff");
let managed = vec![
"app.exe".to_string(),
"plugins".to_string(),
"resources".to_string(),
];
create_payload(&old, b"old-main", b"old-resource");
create_payload(&new, b"new-main", b"new-resource");
fs::write(old.join("uninstall.exe"), b"unmanaged").expect("unmanaged");
let source = build_file_tree_manifest(&old, &managed).expect("source");
let target = build_file_tree_manifest(&new, &managed).expect("target");
let patch_result = create_directory_patch(&CreateDirectoryPatchOptions {
old_path: old.clone(),
new_path: new,
patch_path: patch.clone(),
managed_paths: managed.clone(),
hdiffz_path: Some(hdiffz),
force: true,
step_size: Some("64k".to_string()),
compression: Some("lzma2-9-8m".to_string()),
checksum: Some("xxh128".to_string()),
parallel_threads: Some(2),
})
.expect("patch");
let manifest_path = server.join("latest.json");
fs::write(
&manifest_path,
serde_json::to_vec_pretty(&DirectoryUpdateManifest {
schema_version: DIRECTORY_UPDATE_SCHEMA_VERSION,
version: "0.2.0".to_string(),
notes: None,
published_at: None,
signature: None,
platforms: BTreeMap::from([(
"windows-x86_64".to_string(),
DirectoryPlatformRelease {
full: Artifact {
url: "full.exe".to_string(),
sha256: "a".repeat(64),
size: 1000,
signature: None,
},
managed_paths: managed.clone(),
target: target.clone(),
deltas: vec![DirectoryDelta {
from_version: "0.1.0".to_string(),
source,
patch: Artifact {
url: "payload.hdiff".to_string(),
sha256: patch_result.patch.sha256,
size: patch_result.patch.size,
signature: None,
},
algorithm: DIRECTORY_UPDATE_ALGORITHM.to_string(),
}],
},
)]),
})
.expect("manifest"),
)
.expect("write manifest");
let options = PrepareDirectoryUpdateOptions {
manifest_url: manifest_path.to_string_lossy().to_string(),
application_id: "test.app".to_string(),
install_root: old,
managed_paths: managed,
current_version: "0.1.0".to_string(),
expected_version: "0.2.0".to_string(),
platform: Some("windows-x86_64".to_string()),
cache_dir: cache,
hpatchz_path: Some(hpatchz),
signature_public_keys: Vec::new(),
require_signature: false,
headers: Vec::new(),
timeout_secs: None,
};
let first = prepare_directory_update(options.clone(), |_| {})
.await
.expect("first preparation");
assert_eq!(first.kind, PreparedDirectoryUpdateKind::Prepared);
let second = prepare_directory_update(options, |_| {})
.await
.expect("reuse preparation");
assert_eq!(second.kind, PreparedDirectoryUpdateKind::AlreadyPrepared);
assert_eq!(second.bytes_downloaded, 0);
assert_eq!(
first.transaction.transaction_id,
second.transaction.transaction_id
);
}
fn create_payload(root: &std::path::Path, main: &[u8], resource: &[u8]) {
fs::create_dir_all(root.join("resources")).expect("resources");
fs::create_dir_all(root.join("plugins")).expect("plugins");
fs::write(root.join("app.exe"), main).expect("app");
fs::write(root.join("resources/data.bin"), resource).expect("resource");
fs::write(root.join("plugins/plugin.js"), b"same-plugin").expect("plugin");
}
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|path| path.parent())
.expect("workspace root")
.to_path_buf()
}