use std::io::{Read, Seek, SeekFrom};
use crate::error::VmdkError;
pub(crate) const COWD_MAGIC: u32 = 0x434F_5744;
const COWD_GD_SECTOR: u32 = 4;
pub(crate) const COWD_GTES_PER_GT: usize = 4096;
const SECTOR_SIZE: u64 = 512;
pub(crate) struct CowdHeader {
pub capacity: u32, pub grain_size: u32, }
impl CowdHeader {
pub fn parse(data: &[u8]) -> Result<Self, VmdkError> {
if data.len() < 32 {
return Err(VmdkError::FileTooSmall);
}
let magic = u32::from_be_bytes(data[0..4].try_into().expect("4 bytes"));
if magic != COWD_MAGIC {
return Err(VmdkError::BadMagic);
}
let version = u32::from_le_bytes(data[4..8].try_into().expect("4 bytes"));
if version != 1 {
return Err(VmdkError::UnsupportedVersion(version));
}
let capacity = u32::from_le_bytes(data[12..16].try_into().expect("4 bytes"));
let grain_size = u32::from_le_bytes(data[16..20].try_into().expect("4 bytes"));
if grain_size == 0 {
return Err(VmdkError::FieldOutOfRange {
field: "grain_size",
value: u64::from(grain_size),
reason: "must be > 0",
});
}
Ok(CowdHeader {
capacity,
grain_size,
})
}
}
pub(crate) fn open_cowd<R: Read + Seek>(mut reader: R) -> Result<(Vec<u32>, u64), VmdkError> {
let mut hdr_bytes = [0u8; 512];
reader.read_exact(&mut hdr_bytes)?;
let hdr = CowdHeader::parse(&hdr_bytes)?;
let grain_size_bytes =
u64::from(hdr.grain_size)
.checked_mul(SECTOR_SIZE)
.ok_or(VmdkError::GeometryOverflow {
field: "grain_size",
})?;
let num_grains = u64::from(hdr.capacity).div_ceil(u64::from(hdr.grain_size));
let num_gts = num_grains.div_ceil(COWD_GTES_PER_GT as u64);
let gd_bytes = (num_gts * 4) as usize;
let gd_offset = u64::from(COWD_GD_SECTOR) * SECTOR_SIZE;
reader.seek(SeekFrom::Start(gd_offset))?;
let mut buf = vec![0u8; gd_bytes];
reader.read_exact(&mut buf)?;
let grain_dir = buf
.chunks_exact(4)
.map(|c| u32::from_le_bytes(c.try_into().expect("4")))
.collect();
Ok((grain_dir, grain_size_bytes))
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
fn make_cowd_header(capacity: u32, grain_size: u32, gd_entries: u32) -> Vec<u8> {
let mut h = vec![0u8; 512];
h[0..4].copy_from_slice(&COWD_MAGIC.to_be_bytes());
h[4..8].copy_from_slice(&1u32.to_le_bytes());
h[12..16].copy_from_slice(&capacity.to_le_bytes());
h[16..20].copy_from_slice(&grain_size.to_le_bytes());
h[20..24].copy_from_slice(&4u32.to_le_bytes());
h[24..28].copy_from_slice(&gd_entries.to_le_bytes());
h[28..32].copy_from_slice(&100u32.to_le_bytes());
h
}
#[test]
fn cowd_header_parse_ok() {
let h = make_cowd_header(1024, 8, 1);
let hdr = CowdHeader::parse(&h).expect("parse");
assert_eq!(hdr.capacity, 1024);
assert_eq!(hdr.grain_size, 8);
}
#[test]
fn cowd_header_bad_magic_rejected() {
let h = vec![0u8; 512];
assert!(matches!(CowdHeader::parse(&h), Err(VmdkError::BadMagic)));
}
#[test]
fn cowd_header_short_buffer_rejected() {
assert!(matches!(
CowdHeader::parse(&[0u8; 16]),
Err(VmdkError::FileTooSmall)
));
}
#[test]
fn cowd_header_wrong_version_rejected() {
let mut h = make_cowd_header(1024, 8, 1);
h[4..8].copy_from_slice(&2u32.to_le_bytes()); assert!(matches!(
CowdHeader::parse(&h),
Err(VmdkError::UnsupportedVersion(2))
));
}
#[test]
fn cowd_header_zero_grain_rejected() {
let h = make_cowd_header(1024, 0, 1);
assert!(matches!(
CowdHeader::parse(&h),
Err(VmdkError::FieldOutOfRange {
field: "grain_size",
..
})
));
}
#[test]
fn open_cowd_all_sparse_returns_empty_gd() {
let capacity = 8u32; let grain_size = 8u32;
let h = make_cowd_header(capacity, grain_size, 1);
let mut bytes = h;
bytes.extend_from_slice(&vec![0u8; 512 * 3]);
let mut gd = vec![0u8; 512];
gd[0..4].copy_from_slice(&5u32.to_le_bytes());
bytes.extend_from_slice(&gd);
bytes.extend_from_slice(&vec![0u8; COWD_GTES_PER_GT * 4]);
let (grain_dir, gsz) = open_cowd(Cursor::new(bytes)).expect("open_cowd");
assert_eq!(grain_dir.len(), 1);
assert_eq!(grain_dir[0], 5); assert_eq!(gsz, 8 * 512);
}
}