mirror of
https://gitea.tendokyu.moe/beerpsi/fsdecrypt.git
synced 2026-09-27 09:23:28 +03:00
fix: resolve VHD chains via parent/own GUIDs
This commit is contained in:
+81
-8
@@ -410,10 +410,14 @@ fn main() -> Result<()> {
|
||||
|
||||
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();
|
||||
let deltas: Vec<_> = vhds
|
||||
.iter()
|
||||
.filter(|v| v.sequence_number > 0)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
if deltas.is_empty() {
|
||||
// Standalone base VHD, just extract
|
||||
// Standalone base VHD, just extract.
|
||||
if let Some(base) = base {
|
||||
let output_dir = base.input_path.with_extension("");
|
||||
if let Err(e) = vhd::extract_vhd(&base.vhd_path, &output_dir) {
|
||||
@@ -429,15 +433,84 @@ fn main() -> Result<()> {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Merge + extract each delta with the base (pure Rust, no admin needed)
|
||||
for delta in &deltas {
|
||||
let output_dir = delta.input_path.with_extension("");
|
||||
if let Err(e) = vhd::extract_merged_vhd(&base.vhd_path, &delta.vhd_path, &output_dir) {
|
||||
println!("WARNING: Merged VHD extraction failed: {e:#}");
|
||||
}
|
||||
if let Err(e) = process_chain(base, &deltas) {
|
||||
println!("WARNING: VHD chain processing failed for {game_id}: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Order deltas into a single parent→child chain by matching each delta's
|
||||
/// Parent Unique ID to the previous VHD's own Unique Id, then extract each
|
||||
/// patch level and finally delete the intermediate .vhd files.
|
||||
fn process_chain(base: &ExtractedVhd, deltas: &[&ExtractedVhd]) -> Result<()> {
|
||||
// Collect GUID info for base + all deltas up front (cheap — reads at most
|
||||
// ~1.5 KiB per VHD). Track each VHD by its own GUID.
|
||||
let base_info = vhd::read_vhd_guid_info(&base.vhd_path)
|
||||
.map_err(|e| anyhow!("reading base {}: {e}", base.vhd_path.display()))?;
|
||||
|
||||
let mut remaining: Vec<(vhd::VhdGuidInfo, &ExtractedVhd)> = Vec::with_capacity(deltas.len());
|
||||
for d in deltas {
|
||||
match vhd::read_vhd_guid_info(&d.vhd_path) {
|
||||
Ok(info) => remaining.push((info, *d)),
|
||||
Err(e) => {
|
||||
println!(
|
||||
"WARNING: could not read VHD metadata for {}: {e} — skipping this delta",
|
||||
d.vhd_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the chain: repeatedly look for the delta whose parent_id matches
|
||||
// the last VHD's own_id. Stop if the link breaks so we can warn clearly.
|
||||
let mut chain: Vec<&ExtractedVhd> = vec![base];
|
||||
let mut last_own_id = base_info.own_id;
|
||||
while !remaining.is_empty() {
|
||||
let pos = remaining
|
||||
.iter()
|
||||
.position(|(info, _)| info.parent_id == Some(last_own_id));
|
||||
let Some(pos) = pos else { break };
|
||||
let (info, vhd) = remaining.remove(pos);
|
||||
chain.push(vhd);
|
||||
last_own_id = info.own_id;
|
||||
}
|
||||
|
||||
if !remaining.is_empty() {
|
||||
println!(
|
||||
"WARNING: {} delta(s) could not be linked into the chain (missing parent VHD). \
|
||||
Make sure every intermediate patch is provided.",
|
||||
remaining.len()
|
||||
);
|
||||
for (_, v) in &remaining {
|
||||
println!(" orphan: {}", v.input_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
// chain[0] is the base; chain[i>=1] is a differencing VHD whose correct
|
||||
// merged view is `chain[0..=i]`. Extract each patch level against its
|
||||
// full parent chain.
|
||||
for i in 1..chain.len() {
|
||||
let layers: Vec<&Path> = chain[..=i].iter().map(|v| v.vhd_path.as_path()).collect();
|
||||
let output_dir = chain[i].input_path.with_extension("");
|
||||
if let Err(e) = vhd::extract_chained_vhd(&layers, &output_dir) {
|
||||
println!(
|
||||
"WARNING: chained VHD extraction failed for {}: {e:#}",
|
||||
chain[i].input_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// All extractions done — now safe to delete the intermediate .vhd files.
|
||||
// Includes the base (matches the previous auto-merge behavior of consuming
|
||||
// the extracted VHD once done with it).
|
||||
for v in &chain {
|
||||
if let Err(e) = std::fs::remove_file(&v.vhd_path) {
|
||||
println!("WARNING: Could not delete {}: {e}", v.vhd_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+221
-181
@@ -23,6 +23,8 @@ 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;
|
||||
/// VHD footer Unique Id (GUID), 16 bytes at offset 68 (0x44) — identifies this VHD.
|
||||
const VHD_FOOTER_UNIQUE_ID_OFFSET: usize = 0x44;
|
||||
|
||||
// Dynamic/differencing VHD header
|
||||
const DYNAMIC_HEADER_COOKIE: &[u8; 8] = b"cxsparse";
|
||||
@@ -30,8 +32,64 @@ const DYNAMIC_HEADER_SIZE: usize = 1024;
|
||||
const DYNAMIC_BAT_OFFSET_FIELD: usize = 0x10;
|
||||
const DYNAMIC_MAX_ENTRIES_FIELD: usize = 0x18;
|
||||
const DYNAMIC_BLOCK_SIZE_FIELD: usize = 0x20;
|
||||
/// Dynamic header Parent Unique ID (GUID), 16 bytes at offset 40 (0x28) — only
|
||||
/// meaningful for differencing VHDs; points at the parent VHD's footer Unique Id.
|
||||
const DYNAMIC_PARENT_UNIQUE_ID_OFFSET: usize = 0x28;
|
||||
const BAT_UNUSED: u32 = 0xFFFFFFFF;
|
||||
|
||||
pub type VhdGuid = [u8; 16];
|
||||
|
||||
/// Chain-linking info read from a VHD. `parent_id` is `Some` only for differencing
|
||||
/// disks (type 4), and points at the parent VHD's `own_id`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VhdGuidInfo {
|
||||
pub own_id: VhdGuid,
|
||||
pub parent_id: Option<VhdGuid>,
|
||||
/// Kept for diagnostics / future validation; not every caller inspects it.
|
||||
#[allow(dead_code)]
|
||||
pub disk_type: u32,
|
||||
}
|
||||
|
||||
/// Read a VHD's Unique Id and (for differencing VHDs) its Parent Unique ID.
|
||||
/// Cheap — only reads the 512-byte footer plus, if differencing, the 1024-byte
|
||||
/// dynamic header. Used to build chains by matching child.parent_id -> parent.own_id.
|
||||
pub fn read_vhd_guid_info(path: &Path) -> Result<VhdGuidInfo, VhdError> {
|
||||
let mut f = File::open(path)?;
|
||||
let size = f.seek(SeekFrom::End(0))?;
|
||||
if size < SECTOR_SIZE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
f.seek(SeekFrom::Start(size - SECTOR_SIZE))?;
|
||||
let mut footer = [0u8; SECTOR_SIZE as usize];
|
||||
f.read_exact(&mut footer)?;
|
||||
if &footer[..8] != VHD_COOKIE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
let own_id: VhdGuid = footer[VHD_FOOTER_UNIQUE_ID_OFFSET..VHD_FOOTER_UNIQUE_ID_OFFSET + 16]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let disk_type = read_be_u32(&footer, VHD_FOOTER_DISK_TYPE_OFFSET);
|
||||
|
||||
let parent_id = if disk_type == VHD_TYPE_DIFFERENCING {
|
||||
let header_offset = read_be_u64(&footer, VHD_FOOTER_DATA_OFFSET);
|
||||
f.seek(SeekFrom::Start(header_offset))?;
|
||||
let mut hdr = [0u8; DYNAMIC_HEADER_SIZE];
|
||||
f.read_exact(&mut hdr)?;
|
||||
if &hdr[..8] != DYNAMIC_HEADER_COOKIE {
|
||||
return Err(VhdError::InvalidDynamicHeader);
|
||||
}
|
||||
let guid: VhdGuid = hdr
|
||||
[DYNAMIC_PARENT_UNIQUE_ID_OFFSET..DYNAMIC_PARENT_UNIQUE_ID_OFFSET + 16]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
Some(guid)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(VhdGuidInfo { own_id, parent_id, disk_type })
|
||||
}
|
||||
|
||||
// MBR
|
||||
const MBR_SIGNATURE: [u8; 2] = [0x55, 0xAA];
|
||||
const MBR_PARTITION_TABLE_OFFSET: usize = 0x1BE;
|
||||
@@ -242,130 +300,68 @@ impl<R: Read + Seek> Seek for VhdReader<R> {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Merged VHD reader (base + delta overlay, no disk merge needed)
|
||||
// Chained VHD reader (base + N deltas overlaid, no on-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,
|
||||
/// One layer of a VHD chain: a file handle plus its parsed layout.
|
||||
struct VhdLayer<R> {
|
||||
inner: R,
|
||||
layout: VhdLayout,
|
||||
}
|
||||
|
||||
/// Reads from a chain of VHDs where `layers[0]` is the base (dynamic/fixed)
|
||||
/// and `layers[1..]` are differencing VHDs in parent→child order.
|
||||
///
|
||||
/// For each read, walks layers from top delta down to base. At each layer,
|
||||
/// if the sector is present-and-modified (BAT allocated + bitmap bit set),
|
||||
/// that layer's bytes win; otherwise the read falls through to the layer
|
||||
/// below. The base layer's own `read_at` handles zero-fill for unallocated
|
||||
/// dynamic blocks.
|
||||
pub struct ChainedVhdReader<R> {
|
||||
layers: Vec<VhdLayer<R>>,
|
||||
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 {
|
||||
impl<R: Read + Seek> ChainedVhdReader<R> {
|
||||
/// Build a chain reader. `readers` must be ordered base-first, top-most delta last.
|
||||
pub fn new(readers: Vec<R>) -> Result<Self, VhdError> {
|
||||
if readers.is_empty() {
|
||||
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)?
|
||||
let mut layers: Vec<VhdLayer<R>> = Vec::with_capacity(readers.len());
|
||||
let mut virtual_size = 0u64;
|
||||
|
||||
for (idx, mut r) in readers.into_iter().enumerate() {
|
||||
let file_size = r.seek(SeekFrom::End(0))?;
|
||||
if file_size < SECTOR_SIZE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
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)?
|
||||
r.seek(SeekFrom::Start(file_size - SECTOR_SIZE))?;
|
||||
let mut footer = [0u8; SECTOR_SIZE as usize];
|
||||
r.read_exact(&mut footer)?;
|
||||
if &footer[..8] != VHD_COOKIE {
|
||||
return Err(VhdError::InvalidCookie);
|
||||
}
|
||||
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],
|
||||
)
|
||||
let disk_type = read_be_u32(&footer, VHD_FOOTER_DISK_TYPE_OFFSET);
|
||||
let (layout, vsize) = match disk_type {
|
||||
VHD_TYPE_FIXED => (VhdLayout::Fixed, file_size - SECTOR_SIZE),
|
||||
VHD_TYPE_DYNAMIC | VHD_TYPE_DIFFERENCING => {
|
||||
VhdLayout::parse_sparse(&mut r, &footer)?
|
||||
}
|
||||
t => return Err(VhdError::UnsupportedType(t)),
|
||||
};
|
||||
if idx == 0 {
|
||||
virtual_size = vsize;
|
||||
}
|
||||
layers.push(VhdLayer { inner: r, layout });
|
||||
}
|
||||
|
||||
let ntfs_offset = find_ntfs_offset_chain(&mut layers, virtual_size)?;
|
||||
|
||||
Ok(Self { layers, ntfs_offset, virtual_size, pos: 0 })
|
||||
}
|
||||
|
||||
fn ntfs_size(&self) -> u64 {
|
||||
@@ -373,20 +369,93 @@ impl<R: Read + Seek> MergedVhdReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for MergedVhdReader<R> {
|
||||
/// If `layer` has the sector for `virt_off` present AND marked modified in
|
||||
/// its bitmap, read from it and return `Some(bytes_read)`. Otherwise `None`
|
||||
/// signals "fall through to the layer below".
|
||||
///
|
||||
/// Reads are capped at the current sector boundary. The VHD bitmap is
|
||||
/// per-sector: a single block can have a mixed 1/0 pattern, so a larger read
|
||||
/// might cross a sector that belongs to a different layer. The Read
|
||||
/// implementation loops until `buf` is filled, amortising the extra calls.
|
||||
fn try_read_from_layer<R: Read + Seek>(
|
||||
layer: &mut VhdLayer<R>,
|
||||
virt_off: u64,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<Option<usize>> {
|
||||
match &layer.layout {
|
||||
VhdLayout::Fixed => Ok(None), // Fixed deltas make no sense; fall through.
|
||||
VhdLayout::Sparse { bat, block_size } => {
|
||||
let bi = (virt_off / block_size) as usize;
|
||||
let bo = virt_off % block_size;
|
||||
if bi >= bat.len() || bat[bi] == BAT_UNUSED {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Cap at the current sector to honour per-sector bitmap semantics.
|
||||
let sector_remaining = (SECTOR_SIZE - (virt_off % SECTOR_SIZE)) as usize;
|
||||
let n = std::cmp::min(buf.len(), sector_remaining);
|
||||
let block_file_offset = bat[bi] as u64 * SECTOR_SIZE;
|
||||
|
||||
// Read the block's bitmap sector.
|
||||
layer.inner.seek(SeekFrom::Start(block_file_offset))?;
|
||||
let mut bitmap = [0u8; SECTOR_SIZE as usize];
|
||||
layer.inner.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); // MSB first
|
||||
if (bitmap_byte >> bitmap_bit) & 1 == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let file_off = block_file_offset + SECTOR_SIZE + bo;
|
||||
layer.inner.seek(SeekFrom::Start(file_off))?;
|
||||
let got = layer.inner.read(&mut buf[..n])?;
|
||||
Ok(Some(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk layers top-to-bottom; first layer that owns the sector wins.
|
||||
/// The base layer (index 0) always answers (possibly with zeros for
|
||||
/// unallocated dynamic blocks).
|
||||
fn read_chain<R: Read + Seek>(
|
||||
layers: &mut [VhdLayer<R>],
|
||||
vsize: u64,
|
||||
virt_off: u64,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<usize> {
|
||||
if virt_off >= vsize {
|
||||
return Ok(0);
|
||||
}
|
||||
let cap = std::cmp::min(buf.len() as u64, vsize - virt_off) as usize;
|
||||
|
||||
// Try deltas from top (last) down to just above base (index 1).
|
||||
for i in (1..layers.len()).rev() {
|
||||
if let Some(n) = try_read_from_layer(&mut layers[i], virt_off, &mut buf[..cap])? {
|
||||
return Ok(n);
|
||||
}
|
||||
}
|
||||
// Fall through to base.
|
||||
let base = &mut layers[0];
|
||||
base.layout.read_at(&mut base.inner, virt_off, vsize, &mut buf[..cap])
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for ChainedVhdReader<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])?;
|
||||
let virt_off = self.ntfs_offset + self.pos;
|
||||
let n = read_chain(&mut self.layers, self.virtual_size, virt_off, &mut buf[..cap])?;
|
||||
self.pos += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for MergedVhdReader<R> {
|
||||
impl<R: Read + Seek> Seek for ChainedVhdReader<R> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let target = match pos {
|
||||
SeekFrom::Start(o) => o as i64,
|
||||
@@ -440,66 +509,28 @@ fn find_ntfs_offset<R: Read + Seek>(
|
||||
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,
|
||||
/// Find NTFS offset in a chained view (base + N deltas).
|
||||
fn find_ntfs_offset_chain<R: Read + Seek>(
|
||||
layers: &mut [VhdLayer<R>],
|
||||
vsize: u64,
|
||||
) -> Result<u64, VhdError> {
|
||||
let read_magic = |base: &mut R, delta: &mut R, offset: u64| -> io::Result<[u8; 4]> {
|
||||
// If the chain has only a base, defer to the single-VHD finder — it's simpler
|
||||
// and avoids the bitmap machinery for a pure dynamic/fixed disk.
|
||||
if layers.len() == 1 {
|
||||
let base = &mut layers[0];
|
||||
return find_ntfs_offset(&mut base.inner, &base.layout, vsize);
|
||||
}
|
||||
|
||||
let read_magic = |layers: &mut [VhdLayer<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)?;
|
||||
read_chain(layers, vsize, offset, &mut buf)?;
|
||||
Ok(buf)
|
||||
};
|
||||
|
||||
// Try MBR from merged view
|
||||
// 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)?;
|
||||
read_chain(layers, vsize, 0, &mut mbr)?;
|
||||
|
||||
if mbr[510..512] == MBR_SIGNATURE {
|
||||
for i in 0..MBR_MAX_PARTITIONS {
|
||||
@@ -507,7 +538,7 @@ fn find_ntfs_offset_merged<R: Read + Seek>(
|
||||
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 {
|
||||
if offset + 4 <= vsize && read_magic(layers, offset)? == NTFS_MAGIC {
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
@@ -516,7 +547,7 @@ fn find_ntfs_offset_merged<R: Read + Seek>(
|
||||
}
|
||||
|
||||
for offset in NTFS_PROBE_OFFSETS {
|
||||
if offset + 4 <= vsize && read_magic(base, delta, offset)? == NTFS_MAGIC {
|
||||
if offset + 4 <= vsize && read_magic(layers, offset)? == NTFS_MAGIC {
|
||||
return Ok(offset);
|
||||
}
|
||||
}
|
||||
@@ -673,27 +704,36 @@ pub fn extract_vhd(vhd_path: &Path, output_dir: &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.
|
||||
/// The `output_dir` is where files are extracted to.
|
||||
pub fn extract_merged_vhd(base_path: &Path, delta_path: &Path, output_dir: &Path) -> Result<()> {
|
||||
println!("Extracting merged VHD: {} + {}", base_path.display(), delta_path.display());
|
||||
/// Extract files from a chained view of a base + N differencing VHDs.
|
||||
///
|
||||
/// `chain` must be ordered base-first, top-most delta last. A chain of length 1
|
||||
/// is equivalent to extracting just the base. Unlike [`extract_vhd`], this does
|
||||
/// **not** delete the inputs — the caller is responsible, since a single VHD
|
||||
/// in a chain is typically consumed by multiple extractions (one per patch
|
||||
/// level) and must not be removed until all of them have completed.
|
||||
pub fn extract_chained_vhd(chain: &[&Path], output_dir: &Path) -> Result<()> {
|
||||
if chain.is_empty() {
|
||||
return Err(anyhow!("extract_chained_vhd: empty chain"));
|
||||
}
|
||||
|
||||
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 paths_disp = chain
|
||||
.iter()
|
||||
.map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" + ");
|
||||
println!("Extracting chained VHD: {paths_disp}");
|
||||
|
||||
let readers: Vec<File> = chain
|
||||
.iter()
|
||||
.map(|p| File::open(p))
|
||||
.collect::<io::Result<_>>()?;
|
||||
let mut reader = ChainedVhdReader::new(readers).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let prefix = output_dir.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||
extract_ntfs_to_dir(&mut merged, output_dir, &prefix)?;
|
||||
extract_ntfs_to_dir(&mut reader, 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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user