feat: auto-extract base .app when only delta is provided

When no base VHD is found, fsdecrypt now also searches for the base
.app file ({game_id}_*_0.app) in the same directory, extracts its
VHD, and then merges the delta into it automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jujuforce
2026-03-22 16:40:18 +01:00
co-authored by Claude Opus 4.6
parent 371abf4e32
commit 6b06bc4e92
+66 -24
View File
@@ -33,6 +33,8 @@ struct InputFile {
/// Info about an extracted VHD, used for delta merge.
struct ExtractedVhd {
vhd_path: PathBuf,
/// Original input .app file path (for resolving sibling files)
input_path: PathBuf,
sequence_number: u8,
game_id: String,
}
@@ -367,6 +369,7 @@ fn main() -> Result<()> {
std::str::from_utf8(&bootid.game_id)?.trim_end().to_string();
extracted_vhds.push(ExtractedVhd {
vhd_path,
input_path: path.clone(),
sequence_number: bootid.sequence_number,
game_id,
});
@@ -406,45 +409,84 @@ fn main() -> Result<()> {
continue;
}
// If no base was extracted in this run, search the same directory for an existing one
let found_base_path;
// If no base was extracted in this run, search the same directory
let found_base;
let base = match base {
Some(b) => b,
None => {
// Look for a VHD matching "{game_id}_*_0.vhd" in the same directory as the delta
let delta_dir = deltas[0]
.vhd_path
.input_path
.parent()
.unwrap_or_else(|| Path::new("."));
let pattern_prefix = format!("{game_id}_");
let pattern_suffix = "_0.vhd";
found_base_path = std::fs::read_dir(delta_dir)?
// First, look for an existing base VHD ({game_id}_*_0.vhd)
let existing_vhd = std::fs::read_dir(delta_dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| {
let name = p.file_name().unwrap_or_default().to_string_lossy();
name.starts_with(&pattern_prefix) && name.ends_with(pattern_suffix)
name.starts_with(&pattern_prefix) && name.ends_with("_0.vhd")
});
match &found_base_path {
Some(path) => {
println!("Found existing base VHD: {}", path.display());
// Create a temporary ExtractedVhd to reference
&ExtractedVhd {
vhd_path: path.clone(),
sequence_number: 0,
game_id: game_id.clone(),
if let Some(vhd_path) = existing_vhd {
println!("Found existing base VHD: {}", vhd_path.display());
found_base = ExtractedVhd {
vhd_path,
input_path: PathBuf::new(),
sequence_number: 0,
game_id: game_id.clone(),
};
&found_base
} else {
// Look for a base .app file ({game_id}_*_0.app) and extract it
let base_app = std::fs::read_dir(delta_dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| {
let name = p.file_name().unwrap_or_default().to_string_lossy();
name.starts_with(&pattern_prefix) && name.ends_with("_0.app")
});
match base_app {
Some(app_path) => {
println!(
"Found base APP in same directory, extracting: {}",
app_path.display()
);
// Read the bootid to get the sequence number
let base_file = FscryptDecryptor::new(File::open(&app_path)?)
.map_err(|e| anyhow!(e))?;
let base_seq = base_file.bootid.sequence_number;
drop(base_file);
match extract_internal_vhd(&app_path, base_seq) {
Ok(vhd_path) => {
found_base = ExtractedVhd {
vhd_path,
input_path: app_path,
sequence_number: 0,
game_id: game_id.clone(),
};
&found_base
}
Err(e) => {
println!(
"WARNING: Failed to extract base VHD: {e:#?}"
);
continue;
}
}
}
None => {
println!(
"WARNING: Delta VHD(s) found for {game_id} but no base (seq=0) found."
);
println!(
" Place the base .app or .vhd in the same directory."
);
continue;
}
}
None => {
println!(
"WARNING: Delta VHD(s) found for {game_id} but no base (seq=0) VHD found."
);
println!(
" Provide the base .app file or place the base VHD in the same directory."
);
continue;
}
}
}