mirror of
https://gitea.tendokyu.moe/beerpsi/fsdecrypt.git
synced 2026-09-28 01:37:59 +03:00
feat: pure Rust VHD merge, remove PowerShell dependency
Replace Set-VHD + Merge-VHD PowerShell cmdlets with a pure Rust MergedVhdReader that overlays delta blocks on the base VHD using the sector bitmap to decide which sectors come from which disk. No actual merge to disk needed — reads directly from both files. - No admin privileges or UAC prompts required - No Hyper-V or PowerShell dependency - Cross-platform (works on Windows and Unix) - Remove #[cfg(windows)] from merge/extract path 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
65b070adbd
commit
2121b43699
+8
-76
@@ -222,12 +222,6 @@ struct ExtractedVhd {
|
||||
game_id: String,
|
||||
}
|
||||
|
||||
/// Strip the `\\?\` prefix that `canonicalize` adds on Windows.
|
||||
fn strip_unc_prefix(path: PathBuf) -> PathBuf {
|
||||
let s = path.to_string_lossy();
|
||||
s.strip_prefix(r"\\?\").map(PathBuf::from).unwrap_or(path)
|
||||
}
|
||||
|
||||
/// Search a directory for a file matching `{prefix}*{suffix}`.
|
||||
fn find_sibling(dir: &Path, prefix: &str, suffix: &str) -> Option<PathBuf> {
|
||||
std::fs::read_dir(dir).ok()?
|
||||
@@ -239,57 +233,8 @@ fn find_sibling(dir: &Path, prefix: &str, suffix: &str) -> Option<PathBuf> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Merge a differencing VHD into its parent using Hyper-V PowerShell cmdlets.
|
||||
/// Requires elevation (triggers UAC prompt).
|
||||
#[cfg(windows)]
|
||||
fn merge_vhd(base_vhd: &Path, delta_vhd: &Path) -> Result<()> {
|
||||
let base_abs = strip_unc_prefix(std::fs::canonicalize(base_vhd)?);
|
||||
let delta_abs = strip_unc_prefix(std::fs::canonicalize(delta_vhd)?);
|
||||
|
||||
println!("Merging VHDs: {} <- {}", base_abs.display(), delta_abs.display());
|
||||
|
||||
let ps_commands = format!(
|
||||
"Set-VHD -Path '{}' -ParentPath '{}'; Merge-VHD -Path '{}' -Force",
|
||||
delta_abs.display(),
|
||||
base_abs.display(),
|
||||
delta_abs.display(),
|
||||
);
|
||||
|
||||
let error_log = delta_vhd.with_extension("merge_error.txt");
|
||||
let error_log_abs = strip_unc_prefix(std::path::absolute(&error_log)?);
|
||||
|
||||
let wrapped = format!(
|
||||
"try {{ {} }} catch {{ $_ | Out-File '{}' -Encoding UTF8; throw }}",
|
||||
ps_commands,
|
||||
error_log_abs.display(),
|
||||
);
|
||||
|
||||
let status = std::process::Command::new("powershell")
|
||||
.args([
|
||||
"-Command",
|
||||
&format!(
|
||||
"Start-Process powershell -Verb RunAs -Wait -ArgumentList '-Command', '{}'",
|
||||
wrapped.replace('\'', "''")
|
||||
),
|
||||
])
|
||||
.status()?;
|
||||
|
||||
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 merge failed: {}", error_text.trim());
|
||||
} else if status.success() {
|
||||
println!("Merged into: {}", base_abs.display());
|
||||
} else {
|
||||
println!("WARNING: Merge failed (exit: {status}). Check UAC was accepted.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve the base VHD for a delta: check extracted VHDs, then look for
|
||||
/// an existing .vhd or .app in the same directory.
|
||||
#[cfg(windows)]
|
||||
fn resolve_base_vhd<'a>(
|
||||
base: Option<&'a ExtractedVhd>,
|
||||
game_id: &str,
|
||||
@@ -434,23 +379,23 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Post-extraction: merge deltas and extract VHD contents (Windows only)
|
||||
#[cfg(windows)]
|
||||
// Post-extraction: merge deltas and extract VHD contents
|
||||
if !cli.no_extract && !extracted_vhds.is_empty() {
|
||||
let mut by_game: HashMap<String, Vec<&ExtractedVhd>> = HashMap::new();
|
||||
for vhd in &extracted_vhds {
|
||||
by_game.entry(vhd.game_id.clone()).or_default().push(vhd);
|
||||
}
|
||||
|
||||
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).copied();
|
||||
let deltas: Vec<_> = vhds.iter().filter(|v| v.sequence_number > 0).collect();
|
||||
|
||||
if deltas.is_empty() {
|
||||
// Standalone base VHD, just extract
|
||||
if let Some(base) = base {
|
||||
vhds_to_extract.push(base.vhd_path.clone());
|
||||
if let Err(e) = vhd::extract_vhd(&base.vhd_path) {
|
||||
println!("WARNING: VHD extraction failed: {e:#}");
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -461,23 +406,10 @@ fn main() -> Result<()> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut merge_ok = true;
|
||||
// Merge + extract each delta with the base (pure Rust, no admin needed)
|
||||
for delta in &deltas {
|
||||
if let Err(e) = merge_vhd(&base.vhd_path, &delta.vhd_path) {
|
||||
println!("WARNING: Failed to merge: {e:#}");
|
||||
merge_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if merge_ok {
|
||||
vhds_to_extract.push(base.vhd_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for vhd_path in &vhds_to_extract {
|
||||
if vhd_path.exists() {
|
||||
if let Err(e) = vhd::extract_vhd(vhd_path) {
|
||||
println!("WARNING: VHD extraction failed: {e:#}");
|
||||
if let Err(e) = vhd::extract_merged_vhd(&base.vhd_path, &delta.vhd_path) {
|
||||
println!("WARNING: Merged VHD extraction failed: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+439
-147
@@ -20,10 +20,11 @@ const BUF_SIZE: usize = 256 * 1024;
|
||||
const VHD_COOKIE: &[u8; 8] = b"conectix";
|
||||
const VHD_TYPE_FIXED: u32 = 2;
|
||||
const VHD_TYPE_DYNAMIC: u32 = 3;
|
||||
const VHD_TYPE_DIFFERENCING: u32 = 4;
|
||||
const VHD_FOOTER_DISK_TYPE_OFFSET: usize = 0x3C;
|
||||
const VHD_FOOTER_DATA_OFFSET: usize = 0x10;
|
||||
|
||||
// Dynamic VHD header
|
||||
// Dynamic/differencing VHD header
|
||||
const DYNAMIC_HEADER_COOKIE: &[u8; 8] = b"cxsparse";
|
||||
const DYNAMIC_HEADER_SIZE: usize = 1024;
|
||||
const DYNAMIC_BAT_OFFSET_FIELD: usize = 0x10;
|
||||
@@ -60,7 +61,7 @@ pub enum VhdError {
|
||||
Io(#[from] io::Error),
|
||||
#[error("Not a valid VHD file")]
|
||||
InvalidCookie,
|
||||
#[error("Unsupported VHD type {0} (only fixed and dynamic are supported)")]
|
||||
#[error("Unsupported VHD type {0}")]
|
||||
UnsupportedType(u32),
|
||||
#[error("Invalid dynamic VHD header")]
|
||||
InvalidDynamicHeader,
|
||||
@@ -69,14 +70,106 @@ pub enum VhdError {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VHD reader
|
||||
// VHD layout: how to map virtual offsets to file offsets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum VhdLayout {
|
||||
/// Data is contiguous from offset 0 to (file_size - 512).
|
||||
Fixed,
|
||||
Dynamic { bat: Vec<u32>, block_size: u64 },
|
||||
/// Data is in blocks addressed via a Block Allocation Table.
|
||||
/// Used for both dynamic (type 3) and differencing (type 4) VHDs.
|
||||
Sparse { bat: Vec<u32>, block_size: u64 },
|
||||
}
|
||||
|
||||
impl VhdLayout {
|
||||
/// Parse the dynamic/differencing header and BAT.
|
||||
fn parse_sparse<R: Read + Seek>(inner: &mut R, footer: &[u8]) -> Result<(Self, u64), VhdError> {
|
||||
let header_offset = read_be_u64(footer, VHD_FOOTER_DATA_OFFSET);
|
||||
|
||||
inner.seek(SeekFrom::Start(header_offset))?;
|
||||
let mut hdr = [0u8; DYNAMIC_HEADER_SIZE];
|
||||
inner.read_exact(&mut hdr)?;
|
||||
if &hdr[..8] != DYNAMIC_HEADER_COOKIE {
|
||||
return Err(VhdError::InvalidDynamicHeader);
|
||||
}
|
||||
|
||||
let bat_offset = read_be_u64(&hdr, DYNAMIC_BAT_OFFSET_FIELD);
|
||||
let max_entries = read_be_u32(&hdr, DYNAMIC_MAX_ENTRIES_FIELD) as usize;
|
||||
let block_size = read_be_u32(&hdr, DYNAMIC_BLOCK_SIZE_FIELD) as u64;
|
||||
|
||||
inner.seek(SeekFrom::Start(bat_offset))?;
|
||||
let mut raw = vec![0u8; max_entries * 4];
|
||||
inner.read_exact(&mut raw)?;
|
||||
let bat: Vec<u32> = (0..max_entries).map(|i| read_be_u32(&raw, i * 4)).collect();
|
||||
|
||||
Ok((VhdLayout::Sparse { bat, block_size }, max_entries as u64 * block_size))
|
||||
}
|
||||
|
||||
/// Read bytes from a virtual offset according to this layout.
|
||||
fn read_at<R: Read + Seek>(
|
||||
&self,
|
||||
inner: &mut R,
|
||||
virt_off: u64,
|
||||
virtual_size: u64,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<usize> {
|
||||
if virt_off >= virtual_size {
|
||||
return Ok(0);
|
||||
}
|
||||
let cap = std::cmp::min(buf.len() as u64, virtual_size - virt_off) as usize;
|
||||
|
||||
match self {
|
||||
VhdLayout::Fixed => {
|
||||
inner.seek(SeekFrom::Start(virt_off))?;
|
||||
inner.read(&mut buf[..cap])
|
||||
}
|
||||
VhdLayout::Sparse { bat, block_size } => {
|
||||
let bi = (virt_off / block_size) as usize;
|
||||
let bo = virt_off % block_size;
|
||||
let n = std::cmp::min(cap, (block_size - bo) as usize);
|
||||
|
||||
if bi >= bat.len() || bat[bi] == BAT_UNUSED {
|
||||
buf[..n].fill(0);
|
||||
Ok(n)
|
||||
} else {
|
||||
// Each block: bitmap sector + data. Skip bitmap.
|
||||
let file_off = bat[bi] as u64 * SECTOR_SIZE + SECTOR_SIZE + bo;
|
||||
inner.seek(SeekFrom::Start(file_off))?;
|
||||
inner.read(&mut buf[..n])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read 4 bytes from a virtual offset (for magic-byte probing).
|
||||
fn read_magic<R: Read + Seek>(
|
||||
&self,
|
||||
inner: &mut R,
|
||||
offset: u64,
|
||||
) -> io::Result<[u8; 4]> {
|
||||
let mut buf = [0u8; 4];
|
||||
match self {
|
||||
VhdLayout::Fixed => {
|
||||
inner.seek(SeekFrom::Start(offset))?;
|
||||
inner.read_exact(&mut buf)?;
|
||||
}
|
||||
VhdLayout::Sparse { bat, block_size } => {
|
||||
let bi = (offset / block_size) as usize;
|
||||
if bi < bat.len() && bat[bi] != BAT_UNUSED {
|
||||
let file_off = bat[bi] as u64 * SECTOR_SIZE + SECTOR_SIZE + offset % block_size;
|
||||
inner.seek(SeekFrom::Start(file_off))?;
|
||||
inner.read_exact(&mut buf)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VHD reader (single VHD)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Transparently presents the NTFS partition within a fixed or dynamic VHD.
|
||||
pub struct VhdReader<R> {
|
||||
inner: R,
|
||||
@@ -103,138 +196,16 @@ impl<R: Read + Seek> VhdReader<R> {
|
||||
let disk_type = read_be_u32(&footer, VHD_FOOTER_DISK_TYPE_OFFSET);
|
||||
let (layout, virtual_size) = match disk_type {
|
||||
VHD_TYPE_FIXED => (VhdLayout::Fixed, file_size - SECTOR_SIZE),
|
||||
VHD_TYPE_DYNAMIC => Self::parse_dynamic(&mut inner, &footer)?,
|
||||
VHD_TYPE_DYNAMIC | VHD_TYPE_DIFFERENCING => {
|
||||
VhdLayout::parse_sparse(&mut inner, &footer)?
|
||||
}
|
||||
t => return Err(VhdError::UnsupportedType(t)),
|
||||
};
|
||||
|
||||
let ntfs_offset = Self::find_ntfs(&mut inner, &layout, virtual_size)?;
|
||||
let ntfs_offset = find_ntfs_offset(&mut inner, &layout, virtual_size)?;
|
||||
Ok(Self { inner, layout, ntfs_offset, virtual_size, pos: 0 })
|
||||
}
|
||||
|
||||
fn parse_dynamic(inner: &mut R, footer: &[u8]) -> Result<(VhdLayout, u64), VhdError> {
|
||||
let header_offset = read_be_u64(footer, VHD_FOOTER_DATA_OFFSET);
|
||||
|
||||
inner.seek(SeekFrom::Start(header_offset))?;
|
||||
let mut hdr = [0u8; DYNAMIC_HEADER_SIZE];
|
||||
inner.read_exact(&mut hdr)?;
|
||||
if &hdr[..8] != DYNAMIC_HEADER_COOKIE {
|
||||
return Err(VhdError::InvalidDynamicHeader);
|
||||
}
|
||||
|
||||
let bat_offset = read_be_u64(&hdr, DYNAMIC_BAT_OFFSET_FIELD);
|
||||
let max_entries = read_be_u32(&hdr, DYNAMIC_MAX_ENTRIES_FIELD) as usize;
|
||||
let block_size = read_be_u32(&hdr, DYNAMIC_BLOCK_SIZE_FIELD) as u64;
|
||||
|
||||
inner.seek(SeekFrom::Start(bat_offset))?;
|
||||
let mut raw = vec![0u8; max_entries * 4];
|
||||
inner.read_exact(&mut raw)?;
|
||||
let bat: Vec<u32> = (0..max_entries).map(|i| read_be_u32(&raw, i * 4)).collect();
|
||||
|
||||
Ok((VhdLayout::Dynamic { bat, block_size }, max_entries as u64 * block_size))
|
||||
}
|
||||
|
||||
/// Translate a virtual disk offset to a file read.
|
||||
fn read_virtual(&mut self, virt_off: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if virt_off >= self.virtual_size {
|
||||
return Ok(0);
|
||||
}
|
||||
let cap = std::cmp::min(buf.len() as u64, self.virtual_size - virt_off) as usize;
|
||||
|
||||
match &self.layout {
|
||||
VhdLayout::Fixed => {
|
||||
self.inner.seek(SeekFrom::Start(virt_off))?;
|
||||
self.inner.read(&mut buf[..cap])
|
||||
}
|
||||
VhdLayout::Dynamic { bat, block_size } => {
|
||||
let bi = (virt_off / block_size) as usize;
|
||||
let bo = virt_off % block_size;
|
||||
let n = std::cmp::min(cap, (block_size - bo) as usize);
|
||||
|
||||
if bi >= bat.len() || bat[bi] == BAT_UNUSED {
|
||||
buf[..n].fill(0);
|
||||
Ok(n)
|
||||
} else {
|
||||
// Each block: bitmap sector + data. Skip bitmap.
|
||||
let file_off = bat[bi] as u64 * SECTOR_SIZE + SECTOR_SIZE + bo;
|
||||
self.inner.seek(SeekFrom::Start(file_off))?;
|
||||
self.inner.read(&mut buf[..n])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read 4 bytes from a virtual offset (for magic-byte probing).
|
||||
fn read_virtual_u32(inner: &mut R, layout: &VhdLayout, offset: u64) -> io::Result<[u8; 4]> {
|
||||
let mut buf = [0u8; 4];
|
||||
match layout {
|
||||
VhdLayout::Fixed => {
|
||||
inner.seek(SeekFrom::Start(offset))?;
|
||||
inner.read_exact(&mut buf)?;
|
||||
}
|
||||
VhdLayout::Dynamic { bat, block_size } => {
|
||||
let bi = (offset / block_size) as usize;
|
||||
if bi < bat.len() && bat[bi] != BAT_UNUSED {
|
||||
let file_off = bat[bi] as u64 * SECTOR_SIZE + SECTOR_SIZE + offset % block_size;
|
||||
inner.seek(SeekFrom::Start(file_off))?;
|
||||
inner.read_exact(&mut buf)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Read a full sector from virtual offset 0.
|
||||
fn read_first_sector(inner: &mut R, layout: &VhdLayout) -> io::Result<[u8; SECTOR_SIZE as usize]> {
|
||||
let mut sector = [0u8; SECTOR_SIZE as usize];
|
||||
match layout {
|
||||
VhdLayout::Fixed => {
|
||||
inner.seek(SeekFrom::Start(0))?;
|
||||
inner.read_exact(&mut sector)?;
|
||||
}
|
||||
VhdLayout::Dynamic { bat, .. } => {
|
||||
if !bat.is_empty() && bat[0] != BAT_UNUSED {
|
||||
inner.seek(SeekFrom::Start(bat[0] as u64 * SECTOR_SIZE + SECTOR_SIZE))?;
|
||||
inner.read_exact(&mut sector)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(sector)
|
||||
}
|
||||
|
||||
/// Find the byte offset of the NTFS partition within the virtual disk.
|
||||
fn find_ntfs(inner: &mut R, layout: &VhdLayout, vsize: u64) -> Result<u64, VhdError> {
|
||||
// Try MBR partition table first
|
||||
if vsize >= SECTOR_SIZE {
|
||||
let mbr = Self::read_first_sector(inner, layout)?;
|
||||
|
||||
if mbr[510..512] == MBR_SIGNATURE {
|
||||
for i in 0..MBR_MAX_PARTITIONS {
|
||||
let eo = MBR_PARTITION_TABLE_OFFSET + i * MBR_PARTITION_ENTRY_SIZE;
|
||||
if mbr[eo + 4] == NTFS_PARTITION_TYPE {
|
||||
let lba = u32::from_le_bytes(mbr[eo + 8..eo + 12].try_into().unwrap());
|
||||
let offset = lba as u64 * SECTOR_SIZE;
|
||||
if offset + 4 <= vsize
|
||||
&& Self::read_virtual_u32(inner, layout, offset)? == NTFS_MAGIC
|
||||
{
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probe common offsets
|
||||
for offset in NTFS_PROBE_OFFSETS {
|
||||
if offset + 4 <= vsize
|
||||
&& Self::read_virtual_u32(inner, layout, offset)? == NTFS_MAGIC
|
||||
{
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
|
||||
Err(VhdError::NoNtfsPartition)
|
||||
}
|
||||
|
||||
fn ntfs_size(&self) -> u64 {
|
||||
self.virtual_size - self.ntfs_offset
|
||||
}
|
||||
@@ -247,7 +218,9 @@ impl<R: Read + Seek> Read for VhdReader<R> {
|
||||
return Ok(0);
|
||||
}
|
||||
let cap = std::cmp::min(buf.len() as u64, remaining) as usize;
|
||||
let n = self.read_virtual(self.ntfs_offset + self.pos, &mut buf[..cap])?;
|
||||
let n = self.layout.read_at(
|
||||
&mut self.inner, self.ntfs_offset + self.pos, self.virtual_size, &mut buf[..cap],
|
||||
)?;
|
||||
self.pos += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
@@ -269,7 +242,290 @@ impl<R: Read + Seek> Seek for VhdReader<R> {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NTFS extraction
|
||||
// Merged VHD reader (base + delta overlay, no disk merge needed)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Reads from a differencing VHD overlaid on a base VHD.
|
||||
/// For each block, reads from delta if allocated, otherwise from base.
|
||||
pub struct MergedVhdReader<R> {
|
||||
base: R,
|
||||
base_layout: VhdLayout,
|
||||
delta: R,
|
||||
delta_layout: VhdLayout,
|
||||
ntfs_offset: u64,
|
||||
virtual_size: u64,
|
||||
pos: u64,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> MergedVhdReader<R> {
|
||||
pub fn new(mut base: R, mut delta: R) -> Result<Self, VhdError> {
|
||||
// Parse base
|
||||
let base_file_size = base.seek(SeekFrom::End(0))?;
|
||||
if base_file_size < SECTOR_SIZE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
base.seek(SeekFrom::Start(base_file_size - SECTOR_SIZE))?;
|
||||
let mut base_footer = [0u8; SECTOR_SIZE as usize];
|
||||
base.read_exact(&mut base_footer)?;
|
||||
if &base_footer[..8] != VHD_COOKIE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
|
||||
let base_type = read_be_u32(&base_footer, VHD_FOOTER_DISK_TYPE_OFFSET);
|
||||
let (base_layout, base_vsize) = match base_type {
|
||||
VHD_TYPE_FIXED => (VhdLayout::Fixed, base_file_size - SECTOR_SIZE),
|
||||
VHD_TYPE_DYNAMIC | VHD_TYPE_DIFFERENCING => {
|
||||
VhdLayout::parse_sparse(&mut base, &base_footer)?
|
||||
}
|
||||
t => return Err(VhdError::UnsupportedType(t)),
|
||||
};
|
||||
|
||||
// Parse delta
|
||||
let delta_file_size = delta.seek(SeekFrom::End(0))?;
|
||||
if delta_file_size < SECTOR_SIZE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
delta.seek(SeekFrom::Start(delta_file_size - SECTOR_SIZE))?;
|
||||
let mut delta_footer = [0u8; SECTOR_SIZE as usize];
|
||||
delta.read_exact(&mut delta_footer)?;
|
||||
if &delta_footer[..8] != VHD_COOKIE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
|
||||
let delta_type = read_be_u32(&delta_footer, VHD_FOOTER_DISK_TYPE_OFFSET);
|
||||
let (delta_layout, _) = match delta_type {
|
||||
VHD_TYPE_DYNAMIC | VHD_TYPE_DIFFERENCING => {
|
||||
VhdLayout::parse_sparse(&mut delta, &delta_footer)?
|
||||
}
|
||||
t => return Err(VhdError::UnsupportedType(t)),
|
||||
};
|
||||
|
||||
// Use base's virtual size as the canonical disk size
|
||||
let virtual_size = base_vsize;
|
||||
|
||||
// Find NTFS using the merged view
|
||||
let ntfs_offset = find_ntfs_offset_merged(
|
||||
&mut base, &base_layout, &mut delta, &delta_layout, virtual_size,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
base, base_layout,
|
||||
delta, delta_layout,
|
||||
ntfs_offset, virtual_size, pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read from the merged view: for each sector, check the delta's bitmap
|
||||
/// to decide whether to read from delta or base.
|
||||
fn read_merged(&mut self, virt_off: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if virt_off >= self.virtual_size {
|
||||
return Ok(0);
|
||||
}
|
||||
let cap = std::cmp::min(buf.len() as u64, self.virtual_size - virt_off) as usize;
|
||||
|
||||
match &self.delta_layout {
|
||||
VhdLayout::Fixed => {
|
||||
// Shouldn't happen for a delta, but fall through to base
|
||||
self.base_layout.read_at(&mut self.base, virt_off, self.virtual_size, &mut buf[..cap])
|
||||
}
|
||||
VhdLayout::Sparse { bat, block_size } => {
|
||||
let bi = (virt_off / block_size) as usize;
|
||||
let bo = virt_off % block_size;
|
||||
let n = std::cmp::min(cap, (*block_size - bo) as usize);
|
||||
|
||||
if bi >= bat.len() || bat[bi] == BAT_UNUSED {
|
||||
// Block not in delta, read from base
|
||||
return self.base_layout.read_at(
|
||||
&mut self.base, virt_off, self.virtual_size, &mut buf[..n],
|
||||
);
|
||||
}
|
||||
|
||||
let block_file_offset = bat[bi] as u64 * SECTOR_SIZE;
|
||||
|
||||
// Read the bitmap sector for this block
|
||||
self.delta.seek(SeekFrom::Start(block_file_offset))?;
|
||||
let mut bitmap = [0u8; SECTOR_SIZE as usize];
|
||||
self.delta.read_exact(&mut bitmap)?;
|
||||
|
||||
// Check if the sector containing our offset has been modified
|
||||
let sector_in_block = (bo / SECTOR_SIZE) as usize;
|
||||
let bitmap_byte = bitmap[sector_in_block / 8];
|
||||
let bitmap_bit = 7 - (sector_in_block % 8); // MSB first
|
||||
let sector_modified = (bitmap_byte >> bitmap_bit) & 1 == 1;
|
||||
|
||||
if sector_modified {
|
||||
// Read from delta (skip bitmap sector)
|
||||
let file_off = block_file_offset + SECTOR_SIZE + bo;
|
||||
self.delta.seek(SeekFrom::Start(file_off))?;
|
||||
self.delta.read(&mut buf[..n])
|
||||
} else {
|
||||
// Sector not modified in delta, read from base
|
||||
self.base_layout.read_at(
|
||||
&mut self.base, virt_off, self.virtual_size, &mut buf[..n],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ntfs_size(&self) -> u64 {
|
||||
self.virtual_size - self.ntfs_offset
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for MergedVhdReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let remaining = self.ntfs_size().saturating_sub(self.pos);
|
||||
if remaining == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let cap = std::cmp::min(buf.len() as u64, remaining) as usize;
|
||||
let n = self.read_merged(self.ntfs_offset + self.pos, &mut buf[..cap])?;
|
||||
self.pos += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for MergedVhdReader<R> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let target = match pos {
|
||||
SeekFrom::Start(o) => o as i64,
|
||||
SeekFrom::Current(o) => self.pos as i64 + o,
|
||||
SeekFrom::End(o) => self.ntfs_size() as i64 + o,
|
||||
};
|
||||
if target < 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "seek before start"));
|
||||
}
|
||||
self.pos = target as u64;
|
||||
Ok(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NTFS partition detection (shared between single and merged readers)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Find NTFS offset in a single VHD.
|
||||
fn find_ntfs_offset<R: Read + Seek>(
|
||||
inner: &mut R,
|
||||
layout: &VhdLayout,
|
||||
vsize: u64,
|
||||
) -> Result<u64, VhdError> {
|
||||
// Try MBR
|
||||
if vsize >= SECTOR_SIZE {
|
||||
let mut mbr = [0u8; SECTOR_SIZE as usize];
|
||||
let _ = layout.read_at(inner, 0, vsize, &mut mbr);
|
||||
|
||||
if mbr[510..512] == MBR_SIGNATURE {
|
||||
for i in 0..MBR_MAX_PARTITIONS {
|
||||
let eo = MBR_PARTITION_TABLE_OFFSET + i * MBR_PARTITION_ENTRY_SIZE;
|
||||
if mbr[eo + 4] == NTFS_PARTITION_TYPE {
|
||||
let lba = u32::from_le_bytes(mbr[eo + 8..eo + 12].try_into().unwrap());
|
||||
let offset = lba as u64 * SECTOR_SIZE;
|
||||
if offset + 4 <= vsize && layout.read_magic(inner, offset)? == NTFS_MAGIC {
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probe common offsets
|
||||
for offset in NTFS_PROBE_OFFSETS {
|
||||
if offset + 4 <= vsize && layout.read_magic(inner, offset)? == NTFS_MAGIC {
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
|
||||
Err(VhdError::NoNtfsPartition)
|
||||
}
|
||||
|
||||
/// Read from merged view with bitmap awareness: if the delta has the block
|
||||
/// allocated but the specific sector's bitmap bit is 0, read from base instead.
|
||||
fn read_merged_sector<R: Read + Seek>(
|
||||
base: &mut R,
|
||||
base_layout: &VhdLayout,
|
||||
delta: &mut R,
|
||||
delta_layout: &VhdLayout,
|
||||
vsize: u64,
|
||||
virt_off: u64,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<usize> {
|
||||
match delta_layout {
|
||||
VhdLayout::Fixed => base_layout.read_at(base, virt_off, vsize, buf),
|
||||
VhdLayout::Sparse { bat, block_size } => {
|
||||
let bi = (virt_off / block_size) as usize;
|
||||
if bi >= bat.len() || bat[bi] == BAT_UNUSED {
|
||||
return base_layout.read_at(base, virt_off, vsize, buf);
|
||||
}
|
||||
let bo = virt_off % block_size;
|
||||
let block_file_offset = bat[bi] as u64 * SECTOR_SIZE;
|
||||
|
||||
// Read bitmap
|
||||
delta.seek(SeekFrom::Start(block_file_offset))?;
|
||||
let mut bitmap = [0u8; SECTOR_SIZE as usize];
|
||||
delta.read_exact(&mut bitmap)?;
|
||||
|
||||
let sector_in_block = (bo / SECTOR_SIZE) as usize;
|
||||
let bitmap_byte = bitmap[sector_in_block / 8];
|
||||
let bitmap_bit = 7 - (sector_in_block % 8);
|
||||
let modified = (bitmap_byte >> bitmap_bit) & 1 == 1;
|
||||
|
||||
if modified {
|
||||
let file_off = block_file_offset + SECTOR_SIZE + bo;
|
||||
delta.seek(SeekFrom::Start(file_off))?;
|
||||
delta.read(buf)
|
||||
} else {
|
||||
base_layout.read_at(base, virt_off, vsize, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find NTFS offset in a merged (base + delta) view.
|
||||
fn find_ntfs_offset_merged<R: Read + Seek>(
|
||||
base: &mut R,
|
||||
base_layout: &VhdLayout,
|
||||
delta: &mut R,
|
||||
delta_layout: &VhdLayout,
|
||||
vsize: u64,
|
||||
) -> Result<u64, VhdError> {
|
||||
let read_magic = |base: &mut R, delta: &mut R, offset: u64| -> io::Result<[u8; 4]> {
|
||||
let mut buf = [0u8; 4];
|
||||
read_merged_sector(base, base_layout, delta, delta_layout, vsize, offset, &mut buf)?;
|
||||
Ok(buf)
|
||||
};
|
||||
|
||||
// Try MBR from merged view
|
||||
if vsize >= SECTOR_SIZE {
|
||||
let mut mbr = [0u8; SECTOR_SIZE as usize];
|
||||
read_merged_sector(base, base_layout, delta, delta_layout, vsize, 0, &mut mbr)?;
|
||||
|
||||
if mbr[510..512] == MBR_SIGNATURE {
|
||||
for i in 0..MBR_MAX_PARTITIONS {
|
||||
let eo = MBR_PARTITION_TABLE_OFFSET + i * MBR_PARTITION_ENTRY_SIZE;
|
||||
if mbr[eo + 4] == NTFS_PARTITION_TYPE {
|
||||
let lba = u32::from_le_bytes(mbr[eo + 8..eo + 12].try_into().unwrap());
|
||||
let offset = lba as u64 * SECTOR_SIZE;
|
||||
if offset + 4 <= vsize && read_magic(base, delta, offset)? == NTFS_MAGIC {
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for offset in NTFS_PROBE_OFFSETS {
|
||||
if offset + 4 <= vsize && read_magic(base, delta, offset)? == NTFS_MAGIC {
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
|
||||
Err(VhdError::NoNtfsPartition)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NTFS extraction (shared logic)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn is_ntfs_system_entry(name: &str) -> bool {
|
||||
@@ -375,26 +631,38 @@ fn calculate_ntfs_size<T: Read + Seek>(
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
/// Extract all files from a VHD's NTFS filesystem, then delete the VHD.
|
||||
/// Shared extraction logic: given an NTFS-bearing Read+Seek, extract to output_dir.
|
||||
fn extract_ntfs_to_dir<T: Read + Seek>(fs: &mut T, output_dir: &Path, prefix: &str) -> Result<()> {
|
||||
let mut ntfs = Ntfs::new(fs)?;
|
||||
ntfs.read_upcase_table(fs)?;
|
||||
|
||||
let root = ntfs.root_directory(fs)?;
|
||||
let total = calculate_ntfs_size(&ntfs, fs, &root)?;
|
||||
|
||||
let pb = ProgressBar::new(total)
|
||||
.with_style(ProgressStyle::default_bar().template(PROGRESS_STYLE)?);
|
||||
pb.set_prefix(prefix.to_string());
|
||||
|
||||
create_dir_all(output_dir)?;
|
||||
let root = ntfs.root_directory(fs)?;
|
||||
extract_ntfs_dir(&ntfs, fs, &root, output_dir, &pb)?;
|
||||
pb.finish();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Extract all files from a single VHD's NTFS filesystem, then delete the VHD.
|
||||
pub fn extract_vhd(vhd_path: &Path) -> Result<()> {
|
||||
let output_dir = vhd_path.with_extension("");
|
||||
println!("Extracting VHD: {}", vhd_path.display());
|
||||
|
||||
let mut vhd = VhdReader::new(File::open(vhd_path)?).map_err(|e| anyhow!(e))?;
|
||||
let mut ntfs = Ntfs::new(&mut vhd)?;
|
||||
ntfs.read_upcase_table(&mut vhd)?;
|
||||
|
||||
let root = ntfs.root_directory(&mut vhd)?;
|
||||
let total = calculate_ntfs_size(&ntfs, &mut vhd, &root)?;
|
||||
|
||||
let pb = ProgressBar::new(total)
|
||||
.with_style(ProgressStyle::default_bar().template(PROGRESS_STYLE)?);
|
||||
pb.set_prefix(vhd_path.file_name().unwrap_or_default().to_string_lossy().to_string());
|
||||
|
||||
create_dir_all(&output_dir)?;
|
||||
let root = ntfs.root_directory(&mut vhd)?;
|
||||
extract_ntfs_dir(&ntfs, &mut vhd, &root, &output_dir, &pb)?;
|
||||
pb.finish();
|
||||
let prefix = vhd_path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||
extract_ntfs_to_dir(&mut vhd, &output_dir, &prefix)?;
|
||||
|
||||
println!("Extracted to: {}", output_dir.display());
|
||||
|
||||
@@ -405,6 +673,30 @@ pub fn extract_vhd(vhd_path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract files from a merged view of base + delta VHDs (pure Rust, no admin needed).
|
||||
/// Reads delta blocks where available, falls back to base. Deletes both VHDs after.
|
||||
pub fn extract_merged_vhd(base_path: &Path, delta_path: &Path) -> Result<()> {
|
||||
let output_dir = base_path.with_extension("");
|
||||
println!("Extracting merged VHD: {} + {}", base_path.display(), delta_path.display());
|
||||
|
||||
let base = File::open(base_path)?;
|
||||
let delta = File::open(delta_path)?;
|
||||
let mut merged = MergedVhdReader::new(base, delta).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let prefix = base_path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||
extract_ntfs_to_dir(&mut merged, &output_dir, &prefix)?;
|
||||
|
||||
println!("Extracted to: {}", output_dir.display());
|
||||
|
||||
drop(merged);
|
||||
for path in [base_path, delta_path] {
|
||||
if let Err(e) = std::fs::remove_file(path) {
|
||||
println!("WARNING: Could not delete {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user