vmdk 0.2.0

Pure-Rust read-only VMware VMDK disk image reader (monolithicSparse, streamOptimized, twoGbMaxExtentFlat/Sparse, monolithicFlat)
Documentation
//! Minimal valid sparse VMDK builder for use in tests and downstream crates.

use super::header::{MAGIC, SECTOR_SIZE, VERSION, VERSION_STREAM_OPT};

// Layout constants (all in sectors unless noted):
const DESCRIPTOR_OFFSET: u64 = 1;
const DESCRIPTOR_SECTORS: u64 = 20;
const GD_SECTOR: u64 = DESCRIPTOR_OFFSET + DESCRIPTOR_SECTORS; // 21
const RGD_SECTOR: u64 = GD_SECTOR + 1; // 22
const GT_SECTOR: u64 = RGD_SECTOR + 1; // 23
const GT_SECTORS: u64 = 4; // 512 GTEs × 4 B = 2048 B
const GRAIN_SECTOR: u64 = GT_SECTOR + GT_SECTORS; // 27

pub const GRAIN_SIZE_SECTORS: u64 = 8;
pub const GRAIN_SIZE_BYTES: usize = GRAIN_SIZE_SECTORS as usize * SECTOR_SIZE as usize;
const NUM_GTES_PER_GT: u32 = 512;

/// Build a minimal valid monolithic sparse VMDK containing `sector_data` in grain 0.
///
/// `sector_data` is zero-padded or truncated to [`GRAIN_SIZE_BYTES`].
#[cfg_attr(not(any(test, feature = "test-helpers")), allow(dead_code))]
pub fn test_sparse_vmdk(sector_data: &[u8]) -> Vec<u8> {
    // ── Grain data ────────────────────────────────────────────────────────────
    let mut grain = vec![0u8; GRAIN_SIZE_BYTES];
    let copy_len = sector_data.len().min(GRAIN_SIZE_BYTES);
    grain[..copy_len].copy_from_slice(&sector_data[..copy_len]);

    // ── Header (512 bytes) ────────────────────────────────────────────────────
    let mut hdr = vec![0u8; 512];
    hdr[0..4].copy_from_slice(&MAGIC.to_le_bytes());
    hdr[4..8].copy_from_slice(&VERSION.to_le_bytes());
    hdr[8..12].copy_from_slice(&0u32.to_le_bytes()); // flags
    hdr[12..20].copy_from_slice(&GRAIN_SIZE_SECTORS.to_le_bytes()); // capacity = 1 grain
    hdr[20..28].copy_from_slice(&GRAIN_SIZE_SECTORS.to_le_bytes()); // grainSize
    hdr[28..36].copy_from_slice(&DESCRIPTOR_OFFSET.to_le_bytes());
    hdr[36..44].copy_from_slice(&DESCRIPTOR_SECTORS.to_le_bytes());
    hdr[44..48].copy_from_slice(&NUM_GTES_PER_GT.to_le_bytes());
    hdr[48..56].copy_from_slice(&RGD_SECTOR.to_le_bytes()); // rgdOffset
    hdr[56..64].copy_from_slice(&GD_SECTOR.to_le_bytes()); // gdOffset
    hdr[64..72].copy_from_slice(&GRAIN_SECTOR.to_le_bytes()); // overHead
    hdr[72] = 0; // uncleanShutdown
    hdr[73] = b'\n';
    hdr[74] = b' ';
    hdr[75] = b'\r';
    hdr[76] = b'\n';
    hdr[77..79].copy_from_slice(&0u16.to_le_bytes()); // compressAlgorithm = 0

    // ── Descriptor (20 sectors) ───────────────────────────────────────────────
    let mut desc = vec![0u8; DESCRIPTOR_SECTORS as usize * SECTOR_SIZE as usize];
    let s = "# Disk DescriptorFile\nversion=1\nCID=fffffffe\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n";
    let n = s.len().min(desc.len());
    desc[..n].copy_from_slice(&s.as_bytes()[..n]);

    // ── Grain Directory (1 sector, first entry → GT) ──────────────────────────
    let mut gd = vec![0u8; SECTOR_SIZE as usize];
    gd[0..4].copy_from_slice(&(GT_SECTOR as u32).to_le_bytes());
    let rgd = gd.clone(); // redundant GD

    // ── Grain Table (4 sectors, first GTE → grain data) ──────────────────────
    let mut gt = vec![0u8; GT_SECTORS as usize * SECTOR_SIZE as usize];
    gt[0..4].copy_from_slice(&(GRAIN_SECTOR as u32).to_le_bytes());

    // ── Assemble ──────────────────────────────────────────────────────────────
    let mut vmdk = Vec::new();
    vmdk.extend_from_slice(&hdr);
    vmdk.extend_from_slice(&desc);
    vmdk.extend_from_slice(&gd);
    vmdk.extend_from_slice(&rgd);
    vmdk.extend_from_slice(&gt);
    vmdk.extend_from_slice(&grain);
    vmdk
}

// ── streamOptimized GD_AT_END layout constants ────────────────────────────────
// Sector 0       : primary header (gdOffset = u64::MAX sentinel)
// Sectors 1–20   : descriptor (createType="streamOptimized")
// Sectors 21–24  : GT (512 GTEs, all zero → all-sparse)
// Sector  25      : GD (1 entry → GT sector 21)
// Sector  26      : footer header (real gdOffset = 25)
// Sector  27      : EOS marker (all zeros)
// Total: 28 sectors = 14 336 bytes; 1 MiB virtual disk
const GAE_CAPACITY: u64 = 2048; // 1 MiB in sectors
const GAE_GRAIN_SIZE: u64 = 128; // 64 KiB grain
const GAE_NUM_GTES: u32 = 512;
const GAE_DESC_OFFSET: u64 = 1;
const GAE_DESC_SIZE: u64 = 20;
const GAE_GT_SECTOR: u64 = 21;
const GAE_GD_SECTOR: u64 = 25; // GAE_GT_SECTOR + 4 GT sectors
const GAE_TOTAL_SECTORS: u64 = 28;

// Writes a streamOptimized `SparseExtentHeader` into `h`, varying only `gd_off`.
fn write_stream_opt_hdr(h: &mut [u8; 512], gd_off: u64) {
    h[0..4].copy_from_slice(&MAGIC.to_le_bytes());
    h[4..8].copy_from_slice(&VERSION_STREAM_OPT.to_le_bytes());
    h[8..12].copy_from_slice(&0u32.to_le_bytes()); // flags
    h[12..20].copy_from_slice(&GAE_CAPACITY.to_le_bytes());
    h[20..28].copy_from_slice(&GAE_GRAIN_SIZE.to_le_bytes());
    h[28..36].copy_from_slice(&GAE_DESC_OFFSET.to_le_bytes());
    h[36..44].copy_from_slice(&GAE_DESC_SIZE.to_le_bytes());
    h[44..48].copy_from_slice(&GAE_NUM_GTES.to_le_bytes());
    h[48..56].copy_from_slice(&0u64.to_le_bytes()); // rgdOffset = 0
    h[56..64].copy_from_slice(&gd_off.to_le_bytes());
    h[64..72].copy_from_slice(&GAE_GD_SECTOR.to_le_bytes()); // overHead
    h[72] = 0; // uncleanShutdown
    h[73] = b'\n';
    h[74] = b' ';
    h[75] = b'\r';
    h[76] = b'\n';
    h[77..79].copy_from_slice(&1u16.to_le_bytes()); // compressAlgorithm = 1
}

/// Build a streamOptimized VMDK where the primary header carries `GD_AT_END`
/// (`gdOffset = u64::MAX`) and the real GD is referenced by the footer header
/// pinned at `file_end − 1024`.
///
/// Virtual size is 1 MiB, all grains are sparse (reads return zeros).
#[cfg_attr(not(any(test, feature = "test-helpers")), allow(dead_code))]
pub fn gd_at_end_stream_opt_vmdk() -> Vec<u8> {
    let total_bytes = GAE_TOTAL_SECTORS * SECTOR_SIZE;
    let mut vmdk = vec![0u8; total_bytes as usize];

    // Sector 0: primary header with GD_AT_END sentinel.
    let mut hdr = [0u8; 512];
    write_stream_opt_hdr(&mut hdr, u64::MAX);
    vmdk[0..512].copy_from_slice(&hdr);

    // Sectors 1–20: descriptor.
    let desc = b"# Disk DescriptorFile\nversion=1\nCID=fffffffe\nparentCID=ffffffff\ncreateType=\"streamOptimized\"\n";
    let desc_start = GAE_DESC_OFFSET as usize * SECTOR_SIZE as usize;
    let copy_len = desc.len().min(GAE_DESC_SIZE as usize * SECTOR_SIZE as usize);
    vmdk[desc_start..desc_start + copy_len].copy_from_slice(&desc[..copy_len]);

    // Sectors 21–24: GT (all zeros → all-sparse; already zeroed).

    // Sector 25: GD — single entry pointing to GT at sector 21.
    let gd_start = GAE_GD_SECTOR as usize * SECTOR_SIZE as usize;
    vmdk[gd_start..gd_start + 4].copy_from_slice(&(GAE_GT_SECTOR as u32).to_le_bytes());

    // Sector 26: footer header with real gdOffset = 25.
    let footer_start = (GAE_TOTAL_SECTORS - 2) as usize * SECTOR_SIZE as usize;
    let mut footer = [0u8; 512];
    write_stream_opt_hdr(&mut footer, GAE_GD_SECTOR);
    vmdk[footer_start..footer_start + 512].copy_from_slice(&footer);

    // Sector 27: EOS marker (already all zeros).

    vmdk
}