mirror of
https://gitea.tendokyu.moe/beerpsi/fsdecrypt.git
synced 2026-09-29 02:08:03 +03:00
feat: extract VHD contents to folder and delete VHD
After decryption and merging, VHDs are now mounted via Hyper-V, their contents copied to a same-named folder (excluding system dirs), then dismounted and deleted. Handles partitions without drive letters by assigning one temporarily. Uses a temp .ps1 script to avoid nested PowerShell quoting issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6b06bc4e92
commit
96bb605a17
+116
-2
@@ -234,6 +234,98 @@ fn strip_extended_path_prefix(path: PathBuf) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mount a VHD, extract all contents to a folder with the same name, dismount, and delete the VHD.
|
||||
/// Requires elevation (triggers UAC prompt).
|
||||
#[cfg(windows)]
|
||||
fn extract_vhd_contents(vhd_path: &Path) -> Result<()> {
|
||||
let vhd_abs = strip_extended_path_prefix(std::fs::canonicalize(vhd_path)?);
|
||||
let output_dir = vhd_path.with_extension("");
|
||||
let output_dir_abs = strip_extended_path_prefix(std::path::absolute(&output_dir)?);
|
||||
|
||||
println!("Extracting VHD contents...");
|
||||
println!(" VHD: {}", vhd_abs.display());
|
||||
println!(" Output: {}", output_dir_abs.display());
|
||||
|
||||
// Write a PowerShell script to a temp file to avoid nested quoting hell.
|
||||
// Mount VHD read-only, copy contents, dismount.
|
||||
let script_path = vhd_path.with_extension("extract.ps1");
|
||||
let script_abs = strip_extended_path_prefix(std::path::absolute(&script_path)?);
|
||||
let error_log = vhd_path.with_extension("extract_error.txt");
|
||||
let error_log_abs = strip_extended_path_prefix(std::path::absolute(&error_log)?);
|
||||
|
||||
let script = format!(
|
||||
r#"try {{
|
||||
$vhd = Mount-VHD -Path '{vhd}' -ReadOnly -Passthru
|
||||
$disk = $vhd | Get-Disk
|
||||
$part = $disk | Get-Partition | Where-Object {{ $_.Type -ne 'Reserved' }} | Select-Object -First 1
|
||||
|
||||
# Try to get existing drive letter
|
||||
$dl = ($part | Get-Volume).DriveLetter
|
||||
|
||||
# If no drive letter, assign one temporarily
|
||||
$assignedLetter = $false
|
||||
if (-not $dl) {{
|
||||
$usedLetters = (Get-Volume).DriveLetter
|
||||
$dl = [char[]](90..68) | Where-Object {{ $_ -notin $usedLetters }} | Select-Object -First 1
|
||||
if (-not $dl) {{
|
||||
Dismount-VHD -Path '{vhd}'
|
||||
throw 'No available drive letter'
|
||||
}}
|
||||
$part | Set-Partition -NewDriveLetter $dl
|
||||
$assignedLetter = $true
|
||||
}}
|
||||
|
||||
$src = "$($dl):\"
|
||||
New-Item -ItemType Directory -Force -Path '{out}' | Out-Null
|
||||
Get-ChildItem -Path $src -Force | Where-Object {{ $_.Name -ne 'System Volume Information' -and $_.Name -ne '$Recycle.Bin' }} | Copy-Item -Destination '{out}' -Recurse -Force
|
||||
|
||||
# Remove assigned letter before dismounting
|
||||
if ($assignedLetter) {{
|
||||
$part | Remove-PartitionAccessPath -AccessPath "$($dl):\" -ErrorAction SilentlyContinue
|
||||
}}
|
||||
Dismount-VHD -Path '{vhd}'
|
||||
}} catch {{
|
||||
$_ | Out-File '{err}' -Encoding UTF8
|
||||
try {{ Dismount-VHD -Path '{vhd}' -ErrorAction SilentlyContinue }} catch {{}}
|
||||
throw
|
||||
}}"#,
|
||||
vhd = vhd_abs.display(),
|
||||
out = output_dir_abs.display(),
|
||||
err = error_log_abs.display(),
|
||||
);
|
||||
|
||||
std::fs::write(&script_path, &script)?;
|
||||
|
||||
let status = std::process::Command::new("powershell")
|
||||
.args([
|
||||
"-Command",
|
||||
&format!(
|
||||
"Start-Process powershell -Verb RunAs -Wait -ArgumentList '-ExecutionPolicy', 'Bypass', '-File', '{}'",
|
||||
script_abs.display()
|
||||
),
|
||||
])
|
||||
.status()?;
|
||||
|
||||
// Clean up the script
|
||||
std::fs::remove_file(&script_path).ok();
|
||||
|
||||
if error_log.exists() {
|
||||
let error_text = std::fs::read_to_string(&error_log).unwrap_or_default();
|
||||
std::fs::remove_file(&error_log).ok();
|
||||
println!("WARNING: VHD extraction failed: {}", error_text.trim());
|
||||
} else if status.success() && output_dir.exists() {
|
||||
println!("Extracted to: {}", output_dir_abs.display());
|
||||
// Delete the VHD now that contents are extracted
|
||||
if let Err(e) = std::fs::remove_file(vhd_path) {
|
||||
println!("WARNING: Could not delete VHD: {e}");
|
||||
}
|
||||
} else {
|
||||
println!("WARNING: VHD extraction may have failed. Check UAC was accepted.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge a differencing VHD into its parent using Hyper-V PowerShell cmdlets.
|
||||
///
|
||||
/// Steps:
|
||||
@@ -392,7 +484,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-merge delta VHDs with their base (Windows only)
|
||||
// Post-extraction: merge deltas and extract VHD contents (Windows only)
|
||||
#[cfg(windows)]
|
||||
if !cli.no_extract && !extracted_vhds.is_empty() {
|
||||
// Group by game_id
|
||||
@@ -401,11 +493,18 @@ fn main() -> Result<()> {
|
||||
by_game.entry(vhd.game_id.clone()).or_default().push(vhd);
|
||||
}
|
||||
|
||||
// Track which VHDs to extract contents from at the end
|
||||
let mut vhds_to_extract: Vec<PathBuf> = Vec::new();
|
||||
|
||||
for (game_id, vhds) in &by_game {
|
||||
let base = vhds.iter().find(|v| v.sequence_number == 0);
|
||||
let deltas: Vec<_> = vhds.iter().filter(|v| v.sequence_number > 0).collect();
|
||||
|
||||
if deltas.is_empty() {
|
||||
// Standalone base VHD, just extract its contents
|
||||
if let Some(base) = base {
|
||||
vhds_to_extract.push(base.vhd_path.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -454,7 +553,6 @@ fn main() -> Result<()> {
|
||||
"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;
|
||||
@@ -492,6 +590,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let mut merge_ok = true;
|
||||
for delta in deltas {
|
||||
if let Err(e) = merge_vhd(&base.vhd_path, &delta.vhd_path) {
|
||||
println!(
|
||||
@@ -499,6 +598,21 @@ fn main() -> Result<()> {
|
||||
delta.vhd_path.display(),
|
||||
base.vhd_path.display()
|
||||
);
|
||||
merge_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the final merged VHD
|
||||
if merge_ok {
|
||||
vhds_to_extract.push(base.vhd_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all VHD contents to folders, then delete the VHDs
|
||||
for vhd_path in &vhds_to_extract {
|
||||
if vhd_path.exists() {
|
||||
if let Err(e) = extract_vhd_contents(vhd_path) {
|
||||
println!("WARNING: Failed to extract VHD contents: {e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user