1use std::collections::HashMap;
9use std::fs::File;
10use std::io::{self, BufReader, Read, Seek, SeekFrom};
11use std::path::Path;
12
13mod bytes;
14mod chain;
15mod cowd;
16mod ddb;
17mod descriptor;
18mod diag;
19pub(crate) mod error;
20mod flat;
21pub mod header;
22mod read;
23mod recovery;
24pub mod sesparse;
25mod sparse_multi;
26
27pub use chain::VmdkChainReader;
28pub use ddb::{DiskDatabase, DiskGeometry};
29
30pub use error::VmdkError;
31
32use descriptor::parse_text_descriptor;
33use flat::MultiExtentReader;
34use header::{SparseExtentHeader, GD_AT_END, SECTOR_SIZE};
35use sparse_multi::MultiSparseReader;
36
37pub trait ReadSeek: Read + Seek {}
44impl<T: Read + Seek> ReadSeek for T {}
45
46pub type VmdkFileReader = VmdkReader<Box<dyn ReadSeek + Send>>;
51
52#[derive(Debug, Clone, PartialEq, Eq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct VmdkDigest {
59 pub sha256: String,
61 pub md5: String,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct AllocatedGrain {
71 pub start_lba: u64,
73 pub sector_count: u64,
75}
76
77#[derive(Debug, Clone)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83pub struct VmdkInfo {
84 pub disk_type: String,
86 pub version: u32,
88 pub cid: u32,
90 pub parent_cid: u32,
92 pub grain_size_sectors: u64,
94 pub grain_size_bytes: u64,
96 pub virtual_disk_size: u64,
98 pub sector_count: u64,
100 pub compressed: bool,
102 pub descriptor_text: String,
104 pub disk_database: DiskDatabase,
106}
107
108pub(crate) enum FormatState {
111 Sparse {
112 grain_dir: Vec<u32>,
113 grain_size_bytes: u64,
114 num_gtes_per_gt: u64,
115 compressed: bool,
117 },
118 SeSparse {
120 grain_dir: Vec<u64>,
122 grain_size_bytes: u64,
123 gt_offset_sectors: u64,
125 grains_offset_sectors: u64,
127 },
128 Flat,
130}
131
132pub struct VmdkReader<R: Read + Seek> {
149 pub(crate) inner: R,
150 pub(crate) fmt: FormatState,
151 pub(crate) virtual_disk_size: u64,
152 disk_type: Box<str>,
153 pub(crate) pos: u64,
154 version: u32,
155 cid: u32,
156 parent_cid: u32,
157 descriptor_text: Box<str>,
158 pub(crate) rgd_offset: u64,
160 pub(crate) gd_entry_count: usize,
162 pub(crate) gt_cache: HashMap<u32, Vec<u32>>,
165 pub(crate) rgd_fallback: bool,
168 pub(crate) rgd_recovery_count: u64,
171}
172
173const MAX_DESCRIPTOR_BYTES: u64 = 64 * 1024;
175
176fn read_descriptor<R: Read + Seek>(
182 reader: &mut R,
183 hdr: &SparseExtentHeader,
184) -> io::Result<descriptor::TextDescriptor> {
185 if hdr.descriptor_offset == 0 || hdr.descriptor_size == 0 {
186 return descriptor::parse_text_descriptor("")
187 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()));
188 }
189 let byte_offset = hdr
190 .descriptor_offset
191 .checked_mul(SECTOR_SIZE)
192 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "descriptor_offset overflow"))?;
193 let byte_len = hdr
194 .descriptor_size
195 .checked_mul(SECTOR_SIZE)
196 .unwrap_or(MAX_DESCRIPTOR_BYTES)
197 .min(MAX_DESCRIPTOR_BYTES);
198 reader.seek(SeekFrom::Start(byte_offset))?;
199 let mut buf = vec![0u8; byte_len as usize];
200 reader.read_exact(&mut buf)?;
201
202 let text = descriptor::decode_descriptor(&buf);
203 descriptor::parse_text_descriptor(&text)
204 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
205}
206
207impl<R: Read + Seek> VmdkReader<R> {
208 pub fn open(mut reader: R) -> Result<Self, VmdkError> {
214 let mut hdr_bytes = [0u8; 512];
215 reader.read_exact(&mut hdr_bytes)?;
216
217 let magic_be = u32::from_be_bytes(hdr_bytes[0..4].try_into().expect("4 bytes"));
219 if magic_be == cowd::COWD_MAGIC {
220 return Self::open_cowd(reader, &hdr_bytes);
221 }
222 if hdr_bytes.len() >= 8 {
224 let se_magic = u64::from_le_bytes(hdr_bytes[0..8].try_into().expect("8 bytes"));
225 if se_magic == sesparse::SE_CONST_MAGIC {
226 return Self::open_sesparse(reader, &hdr_bytes);
227 }
228 }
229
230 let hdr = SparseExtentHeader::parse(&hdr_bytes)?;
231
232 let grain_size_bytes =
233 hdr.grain_size
234 .checked_mul(SECTOR_SIZE)
235 .ok_or(VmdkError::GeometryOverflow {
236 field: "grain_size",
237 })?;
238 let virtual_disk_size = hdr
239 .capacity
240 .checked_mul(SECTOR_SIZE)
241 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?;
242
243 let desc = read_descriptor(&mut reader, &hdr)?;
244
245 let num_grains = hdr
246 .capacity
247 .checked_add(hdr.grain_size - 1)
248 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?
249 / hdr.grain_size;
250 let num_gts = num_grains
251 .checked_add(u64::from(hdr.num_gtes_per_gt) - 1)
252 .ok_or(VmdkError::GeometryOverflow {
253 field: "num_grains",
254 })?
255 / u64::from(hdr.num_gtes_per_gt);
256 let gd_byte_len = num_gts.checked_mul(4).ok_or(VmdkError::GeometryOverflow {
257 field: "gd_byte_len",
258 })?;
259
260 const MAX_GD_BYTES: u64 = 16 * 1024 * 1024;
261 if gd_byte_len > MAX_GD_BYTES {
262 return Err(VmdkError::FieldOutOfRange {
263 field: "grain_directory",
264 value: gd_byte_len,
265 reason: "exceeds the 16 MiB cap",
266 });
267 }
268 let gd_offset = if hdr.gd_offset == GD_AT_END {
271 reader.seek(SeekFrom::End(-1024))?;
272 let mut footer_bytes = [0u8; 512];
273 reader.read_exact(&mut footer_bytes)?;
274 SparseExtentHeader::parse(&footer_bytes)?.gd_offset
275 } else {
276 hdr.gd_offset
277 };
278
279 let gd_sector_offset = gd_offset
280 .checked_mul(SECTOR_SIZE)
281 .ok_or(VmdkError::GeometryOverflow { field: "gd_offset" })?;
282 reader.seek(SeekFrom::Start(gd_sector_offset))?;
283 let mut gd_bytes = vec![0u8; gd_byte_len as usize];
284 reader.read_exact(&mut gd_bytes)?;
285
286 let grain_dir = bytes::le_u32_table(&gd_bytes);
287
288 diag::opened(
289 desc.create_type.as_ref(),
290 hdr.version,
291 virtual_disk_size,
292 grain_size_bytes,
293 hdr.compressed,
294 );
295 Ok(VmdkReader {
296 inner: reader,
297 fmt: FormatState::Sparse {
298 grain_dir,
299 grain_size_bytes,
300 num_gtes_per_gt: u64::from(hdr.num_gtes_per_gt),
301 compressed: hdr.compressed,
302 },
303 virtual_disk_size,
304 disk_type: desc.create_type,
305 pos: 0,
306 version: hdr.version,
307 cid: desc.cid,
308 parent_cid: desc.parent_cid,
309 descriptor_text: desc.raw_text,
310 rgd_offset: hdr.rgd_offset,
311 gd_entry_count: num_gts as usize,
312 gt_cache: HashMap::new(),
313 rgd_fallback: false,
314 rgd_recovery_count: 0,
315 })
316 }
317
318 pub fn virtual_disk_size(&self) -> u64 {
320 self.virtual_disk_size
321 }
322
323 pub(crate) fn read_exact_at(&mut self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
326 self.inner.seek(SeekFrom::Start(offset))?;
327 self.inner.read_exact(buf)
328 }
329
330 pub fn disk_type(&self) -> &str {
334 &self.disk_type
335 }
336
337 pub fn cid(&self) -> u32 {
339 self.cid
340 }
341
342 pub fn parent_cid(&self) -> u32 {
344 self.parent_cid
345 }
346
347 pub fn sector_count(&self) -> u64 {
349 self.virtual_disk_size / SECTOR_SIZE
350 }
351
352 pub fn descriptor_text(&self) -> &str {
354 &self.descriptor_text
355 }
356
357 pub fn disk_database(&self) -> DiskDatabase {
362 DiskDatabase::parse(&self.descriptor_text)
363 }
364
365 pub fn change_track_path(&self) -> Option<String> {
369 for line in self.descriptor_text.lines() {
370 if let Some(rest) = line.trim().strip_prefix("changeTrackPath") {
371 let v = rest.trim_start().trim_start_matches('=').trim();
372 let v = v.trim_matches('"');
373 if !v.is_empty() {
374 return Some(v.to_owned());
375 }
376 }
377 }
378 None
379 }
380
381 pub fn effective_content_id(&self) -> String {
386 if self.cid == 0xffff_fffe {
387 if let Some(long) = self.disk_database().long_content_id {
388 return long;
389 }
390 }
391 format!("{:08x}", self.cid)
392 }
393
394 pub fn info(&self) -> VmdkInfo {
396 let (grain_size_sectors, grain_size_bytes, compressed) = match &self.fmt {
397 FormatState::Sparse {
398 grain_size_bytes,
399 compressed,
400 ..
401 } => (
402 *grain_size_bytes / SECTOR_SIZE,
403 *grain_size_bytes,
404 *compressed,
405 ),
406 FormatState::SeSparse {
407 grain_size_bytes, ..
408 } => (*grain_size_bytes / SECTOR_SIZE, *grain_size_bytes, false),
409 FormatState::Flat => (0, 0, false),
410 };
411 VmdkInfo {
412 disk_type: self.disk_type.to_string(),
413 version: self.version,
414 cid: self.cid,
415 parent_cid: self.parent_cid,
416 grain_size_sectors,
417 grain_size_bytes,
418 virtual_disk_size: self.virtual_disk_size,
419 sector_count: self.virtual_disk_size / SECTOR_SIZE,
420 compressed,
421 descriptor_text: self.descriptor_text.to_string(),
422 disk_database: DiskDatabase::parse(&self.descriptor_text),
423 }
424 }
425
426 fn open_sesparse(mut reader: R, hdr_bytes: &[u8]) -> Result<Self, VmdkError> {
430 use sesparse::open_sesparse;
431 reader.seek(SeekFrom::Start(0))?;
432 let (grain_dir, grain_size_bytes, grains_offset_sectors) = open_sesparse(&mut reader)?;
433
434 let se_hdr = sesparse::SeConstHeader::parse(hdr_bytes)?;
435 let virtual_disk_size = se_hdr
436 .capacity
437 .checked_mul(SECTOR_SIZE)
438 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?;
439
440 Ok(VmdkReader {
441 inner: reader,
442 fmt: FormatState::SeSparse {
443 grain_dir,
444 grain_size_bytes,
445 gt_offset_sectors: se_hdr.gt_offset,
446 grains_offset_sectors,
447 },
448 virtual_disk_size,
449 disk_type: Box::from("seSparse"),
450 pos: 0,
451 version: 0,
452 cid: 0xffff_ffff,
453 parent_cid: 0xffff_ffff,
454 descriptor_text: Box::from(""),
455 rgd_offset: 0,
456 gd_entry_count: 0,
457 gt_cache: HashMap::new(),
458 rgd_fallback: false,
459 rgd_recovery_count: 0,
460 })
461 }
462
463 fn open_cowd(mut reader: R, hdr_bytes: &[u8]) -> Result<Self, VmdkError> {
467 use cowd::{open_cowd, COWD_GTES_PER_GT};
468
469 reader.seek(SeekFrom::Start(0))?;
472 let (grain_dir, grain_size_bytes) = open_cowd(&mut reader)?;
473
474 let cowd_hdr = cowd::CowdHeader::parse(hdr_bytes)?;
476 let virtual_disk_size = u64::from(cowd_hdr.capacity)
477 .checked_mul(SECTOR_SIZE)
478 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?;
479
480 Ok(VmdkReader {
481 inner: reader,
482 fmt: FormatState::Sparse {
483 grain_dir,
484 grain_size_bytes,
485 num_gtes_per_gt: COWD_GTES_PER_GT as u64,
486 compressed: false,
487 },
488 virtual_disk_size,
489 disk_type: Box::from("vmfsSparse"),
490 pos: 0,
491 version: 1,
492 cid: 0xffff_ffff,
493 parent_cid: 0xffff_ffff,
494 descriptor_text: Box::from(""),
495 rgd_offset: 0,
496 gd_entry_count: 0,
497 gt_cache: HashMap::new(),
498 rgd_fallback: false,
499 rgd_recovery_count: 0,
500 })
501 }
502
503 pub fn is_allocated(&mut self, lba: u64) -> io::Result<bool> {
509 if lba >= self.virtual_disk_size / SECTOR_SIZE {
510 return Ok(false);
511 }
512 let virtual_offset = lba * SECTOR_SIZE;
514 match &self.fmt {
515 FormatState::Flat => Ok(true),
516 FormatState::Sparse {
517 grain_dir,
518 grain_size_bytes,
519 num_gtes_per_gt,
520 ..
521 } => {
522 let grain_idx = virtual_offset / grain_size_bytes;
523 let gd_idx = (grain_idx / num_gtes_per_gt) as usize;
524 let gte_idx = grain_idx % num_gtes_per_gt;
525 let gt_sector = grain_dir.get(gd_idx).copied().unwrap_or(0);
526 let () = ();
527 if gt_sector == 0 {
528 return Ok(false);
529 }
530 let gte_pos = u64::from(gt_sector) * SECTOR_SIZE + gte_idx * 4;
531 let mut b = [0u8; 4];
532 self.read_exact_at(gte_pos, &mut b)?;
533 Ok(u32::from_le_bytes(b) > 1)
534 }
535 FormatState::SeSparse {
536 grain_dir,
537 grain_size_bytes,
538 gt_offset_sectors,
539 ..
540 } => {
541 let gd_entry = {
542 let grain_idx = virtual_offset / grain_size_bytes;
543 let gd_idx = (grain_idx / sesparse::SE_GTES_PER_GT) as usize;
544 grain_dir.get(gd_idx).copied().unwrap_or(0)
545 };
546 let grain_idx = virtual_offset / grain_size_bytes;
547 let gte_idx = grain_idx % sesparse::SE_GTES_PER_GT;
548 let gt_off = *gt_offset_sectors;
549 let Some(gte) = self.se_read_gte(gd_entry, gt_off, gte_idx)? else {
550 return Ok(false);
551 };
552 Ok(gte & sesparse::SE_GTE_TYPE_MASK == sesparse::SE_GTE_TYPE_ALLOCATED)
554 }
555 }
556 }
557
558 pub(crate) fn se_read_gte(
563 &mut self,
564 gd_entry: u64,
565 gt_offset_sectors: u64,
566 gte_idx: u64,
567 ) -> io::Result<Option<u64>> {
568 if gd_entry == 0 {
569 return Ok(None);
570 }
571 if gd_entry & sesparse::SE_GD_ALLOC_MASK != sesparse::SE_GD_ALLOC_FLAG {
572 return Err(io::Error::new(
573 io::ErrorKind::InvalidData,
574 "seSparse GD entry has invalid allocated marker",
575 ));
576 }
577 let gt_table_idx = gd_entry & sesparse::SE_GD_INDEX_MASK;
578 let gt_sector = gt_offset_sectors + gt_table_idx * sesparse::SE_GT_SECTORS;
579 let gte_pos = gt_sector * SECTOR_SIZE + gte_idx * 8;
580 let mut b = [0u8; 8];
581 self.read_exact_at(gte_pos, &mut b)?;
582 Ok(Some(u64::from_le_bytes(b)))
583 }
584
585 pub fn iter_allocated_grains(&mut self) -> io::Result<Vec<AllocatedGrain>> {
591 let (grain_dir, grain_size_bytes, num_gtes_per_gt) = match &self.fmt {
592 FormatState::Flat => {
593 let sector_count = self.virtual_disk_size / SECTOR_SIZE;
595 return Ok(if sector_count == 0 {
596 vec![]
597 } else {
598 vec![AllocatedGrain {
599 start_lba: 0,
600 sector_count,
601 }]
602 });
603 }
604 FormatState::Sparse {
605 grain_dir,
606 grain_size_bytes,
607 num_gtes_per_gt,
608 ..
609 } => (grain_dir.clone(), *grain_size_bytes, *num_gtes_per_gt),
610 FormatState::SeSparse {
611 grain_dir,
612 grain_size_bytes,
613 gt_offset_sectors,
614 ..
615 } => {
616 let (gd, gsz, goff) = (grain_dir.clone(), *grain_size_bytes, *gt_offset_sectors);
617 let grain_sectors = gsz / SECTOR_SIZE;
618 let max_lba = self.virtual_disk_size / SECTOR_SIZE;
619 let mut result = Vec::new();
620 for (gd_idx, &gd_entry) in gd.iter().enumerate() {
621 if gd_entry == 0 {
623 continue;
624 }
625 if gd_entry & sesparse::SE_GD_ALLOC_MASK != sesparse::SE_GD_ALLOC_FLAG {
626 continue; }
628 let gt_table_idx = gd_entry & sesparse::SE_GD_INDEX_MASK;
629 let gt_sector = goff + gt_table_idx * sesparse::SE_GT_SECTORS;
630 let gt_bytes_len = sesparse::SE_GTES_PER_GT as usize * 8;
631 let mut gt_bytes = vec![0u8; gt_bytes_len];
632 self.read_exact_at(gt_sector * SECTOR_SIZE, &mut gt_bytes)?;
633 for gte_idx in 0..sesparse::SE_GTES_PER_GT as usize {
634 let gte = u64::from_le_bytes(
635 gt_bytes[gte_idx * 8..gte_idx * 8 + 8]
636 .try_into()
637 .expect("8 bytes"),
638 );
639 if gte & sesparse::SE_GTE_TYPE_MASK == sesparse::SE_GTE_TYPE_ALLOCATED {
641 let grain_idx =
642 gd_idx as u64 * sesparse::SE_GTES_PER_GT + gte_idx as u64;
643 let start_lba = grain_idx * grain_sectors;
644 if start_lba < max_lba {
645 result.push(AllocatedGrain {
646 start_lba,
647 sector_count: grain_sectors,
648 });
649 }
650 }
651 }
652 }
653 return Ok(result);
654 }
655 };
656 let grain_sectors = grain_size_bytes / SECTOR_SIZE;
657 let mut result = Vec::new();
658
659 for (gd_idx, &primary_gt_sector) in grain_dir.iter().enumerate() {
660 let gt_sector = if self.rgd_fallback {
664 self.resilient_gt_sector(gd_idx, primary_gt_sector, num_gtes_per_gt)?
665 } else {
666 primary_gt_sector
667 };
668 let redundant_gt = if self.rgd_fallback {
669 self.read_redundant_gt(gd_idx, num_gtes_per_gt)?
670 } else {
671 None
672 };
673 if gt_sector == 0 {
674 continue;
675 }
676 let gt_size = num_gtes_per_gt as usize * 4;
677 let gt_bytes = {
678 let gt_byte_offset = u64::from(gt_sector) * SECTOR_SIZE;
679 let mut b = vec![0u8; gt_size];
680 self.read_exact_at(gt_byte_offset, &mut b)?;
681 b
682 };
683
684 let pointer_recovered =
686 self.rgd_fallback && gt_sector != primary_gt_sector && gt_sector != 0;
687 for gte_idx in 0..num_gtes_per_gt as usize {
688 let mut gte = u32::from_le_bytes(
689 gt_bytes[gte_idx * 4..gte_idx * 4 + 4]
690 .try_into()
691 .expect("4 bytes"),
692 );
693 let mut entry_recovered = false;
695 if gte <= 1 {
696 if let Some(rgt) = &redundant_gt {
697 let rgte = u32::from_le_bytes(
698 rgt[gte_idx * 4..gte_idx * 4 + 4]
699 .try_into()
700 .expect("4 bytes"),
701 );
702 if rgte > 1 {
703 gte = rgte;
704 entry_recovered = true;
705 }
706 }
707 }
708 if gte > 1 {
709 if pointer_recovered || entry_recovered {
710 self.rgd_recovery_count += 1;
711 }
712 let grain_idx = gd_idx as u64 * num_gtes_per_gt + gte_idx as u64;
713 let start_lba = grain_idx * grain_sectors;
714 if start_lba < self.virtual_disk_size / SECTOR_SIZE {
715 result.push(AllocatedGrain {
716 start_lba,
717 sector_count: grain_sectors,
718 });
719 }
720 }
721 }
722 }
723 Ok(result)
724 }
725
726 pub fn hash(&mut self) -> io::Result<VmdkDigest> {
731 use md5::Md5;
732 use sha2::{Digest as _, Sha256};
733
734 let mut sha = Sha256::new();
735 let mut md = Md5::new();
736 let mut buf = vec![0u8; 65536];
737 loop {
738 let n = self.read(&mut buf)?;
739 if n == 0 {
740 break;
741 }
742 sha.update(&buf[..n]);
743 md.update(&buf[..n]);
744 }
745 let sha_bytes = sha.finalize();
746 let md_bytes = md.finalize();
747 Ok(VmdkDigest {
748 sha256: sha_bytes
749 .iter()
750 .fold(String::with_capacity(64), |mut s, b| {
751 use std::fmt::Write as _;
752 let _ = write!(s, "{b:02x}");
753 s
754 }),
755 md5: md_bytes.iter().fold(String::with_capacity(32), |mut s, b| {
756 use std::fmt::Write as _;
757 let _ = write!(s, "{b:02x}");
758 s
759 }),
760 })
761 }
762
763 #[doc(hidden)]
767 pub fn gt_cache_size(&self) -> usize {
768 self.gt_cache.len()
769 }
770}
771
772impl VmdkFileReader {
775 pub fn extent_dependencies(path: &Path) -> Result<Vec<std::path::PathBuf>, VmdkError> {
787 let first_byte = {
789 let mut buf = [0u8; 1];
790 File::open(path)?.read_exact(&mut buf)?;
791 buf[0]
792 };
793 if first_byte != b'#' {
794 return Ok(Vec::new());
795 }
796 let text = std::fs::read_to_string(path)?;
797 let desc = parse_text_descriptor(&text)?;
798 let dir = path.parent().unwrap_or(Path::new("."));
799
800 let mut deps = Vec::new();
801 for ext in &desc.extents {
803 if ext.is_zero || ext.filename.is_empty() {
804 continue;
805 }
806 deps.push(dir.join(ext.filename.as_ref()));
807 }
808 for ext in &desc.sparse_extents {
810 if ext.filename.is_empty() {
811 continue;
812 }
813 deps.push(dir.join(ext.filename.as_ref()));
814 }
815 Ok(deps)
816 }
817
818 pub fn open_path(path: &Path) -> Result<Self, VmdkError> {
824 let first_byte = {
826 let mut buf = [0u8; 1];
827 File::open(path)?.read_exact(&mut buf)?;
828 buf[0]
829 };
830
831 if first_byte == b'#' {
832 let text = descriptor::decode_descriptor(&std::fs::read(path)?);
836 let desc = parse_text_descriptor(&text)?;
837 let dir = path.parent().unwrap_or(Path::new("."));
838
839 match desc.create_type.as_ref() {
840 "vmfs"
844 | "vmfsPreallocated"
845 | "vmfsEagerZeroedThick"
846 | "vmfsRDM"
847 | "vmfsRaw"
848 | "vmfsRawDeviceMap"
849 | "vmfsPassthroughRawDeviceMap"
850 | "fullDevice"
851 | "partitionedDevice"
852 | "twoGbMaxExtentFlat"
853 | "monolithicFlat" => {
854 let multi = MultiExtentReader::open(dir, &desc.extents)?;
855 let virtual_disk_size = desc
856 .capacity_sectors
857 .checked_mul(SECTOR_SIZE)
858 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?;
859 Ok(VmdkReader {
860 inner: Box::new(multi) as Box<dyn ReadSeek + Send>,
861 fmt: FormatState::Flat,
862 virtual_disk_size,
863 disk_type: desc.create_type,
864 pos: 0,
865 version: 0,
866 cid: desc.cid,
867 parent_cid: desc.parent_cid,
868 descriptor_text: desc.raw_text,
869 rgd_offset: 0,
870 gd_entry_count: 0,
871 gt_cache: HashMap::new(),
872 rgd_fallback: false,
873 rgd_recovery_count: 0,
874 })
875 }
876 "vmfsSparse" | "vmfsThin" | "twoGbMaxExtentSparse" => {
878 let multi = MultiSparseReader::open(dir, &desc.sparse_extents)?;
879 let virtual_disk_size =
880 desc.sparse_capacity_sectors
881 .checked_mul(SECTOR_SIZE)
882 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?;
883 Ok(VmdkReader {
884 inner: Box::new(multi) as Box<dyn ReadSeek + Send>,
885 fmt: FormatState::Flat,
886 virtual_disk_size,
887 disk_type: desc.create_type,
888 pos: 0,
889 version: 0,
890 cid: desc.cid,
891 parent_cid: desc.parent_cid,
892 descriptor_text: desc.raw_text,
893 rgd_offset: 0,
894 gd_entry_count: 0,
895 gt_cache: HashMap::new(),
896 rgd_fallback: false,
897 rgd_recovery_count: 0,
898 })
899 }
900 "seSparse" => {
902 let entry =
903 desc.sparse_extents
904 .first()
905 .ok_or(VmdkError::MalformedDescriptor(
906 "seSparse createType without a SESPARSE extent",
907 ))?;
908 let extent_path = dir.join(entry.filename.as_ref());
909 let file = BufReader::new(File::open(&extent_path)?);
910 Ok(VmdkReader::open(file)?.into_file_reader())
911 }
912 "custom" => {
914 if !desc.extents.is_empty() && !desc.sparse_extents.is_empty() {
915 Err(VmdkError::MalformedDescriptor(
918 "custom createType mixes flat and sparse extents, which is not supported",
919 ))
920 } else if !desc.extents.is_empty() {
921 let multi = MultiExtentReader::open(dir, &desc.extents)?;
922 let virtual_disk_size = desc
923 .capacity_sectors
924 .checked_mul(SECTOR_SIZE)
925 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?;
926 Ok(VmdkReader {
927 inner: Box::new(multi) as Box<dyn ReadSeek + Send>,
928 fmt: FormatState::Flat,
929 virtual_disk_size,
930 disk_type: desc.create_type,
931 pos: 0,
932 version: 0,
933 cid: desc.cid,
934 parent_cid: desc.parent_cid,
935 descriptor_text: desc.raw_text,
936 rgd_offset: 0,
937 gd_entry_count: 0,
938 gt_cache: HashMap::new(),
939 rgd_fallback: false,
940 rgd_recovery_count: 0,
941 })
942 } else if !desc.sparse_extents.is_empty() {
943 let multi = MultiSparseReader::open(dir, &desc.sparse_extents)?;
944 let virtual_disk_size = desc
945 .sparse_capacity_sectors
946 .checked_mul(SECTOR_SIZE)
947 .ok_or(VmdkError::GeometryOverflow { field: "capacity" })?;
948 Ok(VmdkReader {
949 inner: Box::new(multi) as Box<dyn ReadSeek + Send>,
950 fmt: FormatState::Flat,
951 virtual_disk_size,
952 disk_type: desc.create_type,
953 pos: 0,
954 version: 0,
955 cid: desc.cid,
956 parent_cid: desc.parent_cid,
957 descriptor_text: desc.raw_text,
958 rgd_offset: 0,
959 gd_entry_count: 0,
960 gt_cache: HashMap::new(),
961 rgd_fallback: false,
962 rgd_recovery_count: 0,
963 })
964 } else {
965 Err(VmdkError::MalformedDescriptor(
966 "custom createType without recognised extents",
967 ))
968 }
969 }
970 _ => Err(VmdkError::UnsupportedDiskType(
971 desc.create_type.into_string(),
972 )),
973 }
974 } else {
975 let file = BufReader::new(File::open(path)?);
977 Ok(VmdkReader::open(file)?.into_file_reader())
978 }
979 }
980}
981
982impl<R: Read + Seek + Send + 'static> VmdkReader<R> {
983 fn into_file_reader(self) -> VmdkFileReader {
984 VmdkFileReader {
985 inner: Box::new(self.inner),
986 fmt: self.fmt,
987 virtual_disk_size: self.virtual_disk_size,
988 disk_type: self.disk_type,
989 pos: self.pos,
990 version: self.version,
991 cid: self.cid,
992 parent_cid: self.parent_cid,
993 descriptor_text: self.descriptor_text,
994 rgd_offset: self.rgd_offset,
995 gd_entry_count: self.gd_entry_count,
996 gt_cache: self.gt_cache,
997 rgd_fallback: self.rgd_fallback,
998 rgd_recovery_count: self.rgd_recovery_count,
999 }
1000 }
1001}
1002
1003#[cfg(feature = "test-helpers")]
1008pub mod testutil;
1009#[cfg(not(feature = "test-helpers"))]
1010mod testutil;
1011
1012#[cfg(feature = "vfs")]
1015mod vfs;
1016#[cfg(feature = "vfs")]
1017pub use vfs::VmdkSource;
1018
1019#[cfg(test)]
1022mod tests {
1023 use super::*;
1024 use std::io::Cursor;
1025 use testutil::{
1026 compressed_vmdk_with_oversized_marker, gd_at_end_stream_opt_vmdk, test_cowd_vmdk,
1027 test_sesparse_vmdk, test_sparse_vmdk, GRAIN_SIZE_BYTES,
1028 };
1029
1030 fn vmdk_header_bytes(capacity_sectors: u64, grain_size: u64, num_gtes_per_gt: u32) -> Vec<u8> {
1031 let mut h = vec![0u8; 512];
1032 h[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1033 h[4..8].copy_from_slice(&1u32.to_le_bytes());
1034 h[12..20].copy_from_slice(&capacity_sectors.to_le_bytes());
1035 h[20..28].copy_from_slice(&grain_size.to_le_bytes());
1036 h[44..48].copy_from_slice(&num_gtes_per_gt.to_le_bytes());
1037 h
1038 }
1039
1040 #[test]
1043 fn header_version_2_zeroed_grain_opens() {
1044 let mut vmdk = test_sparse_vmdk(&[0u8; 512]);
1047 vmdk[4..8].copy_from_slice(&2u32.to_le_bytes()); vmdk[8..12].copy_from_slice(&0x0000_0004u32.to_le_bytes()); VmdkReader::open(Cursor::new(vmdk))
1050 .expect("version=2 (zeroed-grain) monolithicSparse must open");
1051 }
1052
1053 #[test]
1054 fn zero_extent_type_reads_as_zeros() {
1055 use std::io::Write as _;
1058 let dir = tempfile::tempdir().unwrap();
1059 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW 2048 ZERO\n";
1060 let desc_path = dir.path().join("zero.vmdk");
1061 std::fs::File::create(&desc_path)
1062 .unwrap()
1063 .write_all(desc.as_bytes())
1064 .unwrap();
1065 let mut reader =
1066 VmdkFileReader::open_path(&desc_path).expect("descriptor with a ZERO extent must open");
1067 assert_eq!(
1068 reader.virtual_disk_size(),
1069 2048 * 512,
1070 "ZERO extent contributes its sector count"
1071 );
1072 reader.seek(SeekFrom::Start(0)).unwrap();
1073 let mut buf = [0xFFu8; 512];
1074 reader.read_exact(&mut buf).expect("read");
1075 assert_eq!(buf, [0u8; 512], "ZERO extent must read as zeros");
1076 }
1077
1078 fn assert_flat_create_type_reads(create_type: &str, extent_kw: &str, byte0: u8) {
1083 use std::io::Write as _;
1084 let dir = tempfile::tempdir().unwrap();
1085 let mut extent = vec![0u8; 1024];
1086 extent[0] = byte0;
1087 let extent_path = dir.path().join("disk-flat.vmdk");
1088 std::fs::File::create(&extent_path)
1089 .unwrap()
1090 .write_all(&extent)
1091 .unwrap();
1092 let offset = if extent_kw == "FLAT" { " 0" } else { "" };
1093 let desc = format!(
1094 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\n\
1095 createType=\"{create_type}\"\nRW 2 {extent_kw} \"disk-flat.vmdk\"{offset}\n"
1096 );
1097 let desc_path = dir.path().join("disk.vmdk");
1098 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1099 let mut reader = VmdkFileReader::open_path(&desc_path)
1100 .unwrap_or_else(|e| panic!("{create_type}/{extent_kw} must open: {e:?}"));
1101 let mut buf = [0u8; 1];
1102 reader.read_exact(&mut buf).expect("read");
1103 assert_eq!(
1104 buf[0], byte0,
1105 "{create_type}: must read the referenced extent"
1106 );
1107 }
1108
1109 #[test]
1110 fn custom_create_type_with_flat_extent_opens() {
1111 assert_flat_create_type_reads("custom", "FLAT", 0xC0);
1113 }
1114
1115 #[test]
1116 fn full_device_create_type_routes_to_flat() {
1117 assert_flat_create_type_reads("fullDevice", "FLAT", 0xFD);
1120 assert_flat_create_type_reads("partitionedDevice", "FLAT", 0xDE);
1121 }
1122
1123 #[test]
1124 fn vmfs_raw_rdm_create_types_route_to_flat() {
1125 assert_flat_create_type_reads("vmfsRaw", "VMFSRAW", 0x4A);
1128 assert_flat_create_type_reads("vmfsRawDeviceMap", "VMFSRAW", 0x4B);
1129 }
1130
1131 #[test]
1134 fn extent_dependencies_lists_flat_companion() {
1135 use std::io::Write as _;
1138 let dir = tempfile::tempdir().unwrap();
1139 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"twoGbMaxExtentFlat\"\nRW 2048 FLAT \"disk-f001.vmdk\" 0\n";
1140 let desc_path = dir.path().join("disk.vmdk");
1141 std::fs::File::create(&desc_path)
1142 .unwrap()
1143 .write_all(desc.as_bytes())
1144 .unwrap();
1145 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("extent_dependencies");
1146 assert_eq!(deps.len(), 1, "one companion extent");
1147 assert_eq!(
1148 deps[0].file_name().unwrap().to_string_lossy(),
1149 "disk-f001.vmdk"
1150 );
1151 assert_eq!(deps[0].parent().unwrap(), dir.path());
1153 }
1154
1155 #[test]
1156 fn extent_dependencies_lists_sparse_companions() {
1157 use std::io::Write as _;
1158 let dir = tempfile::tempdir().unwrap();
1159 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"twoGbMaxExtentSparse\"\nRW 4194304 SPARSE \"disk-s001.vmdk\"\nRW 4194304 SPARSE \"disk-s002.vmdk\"\n";
1160 let desc_path = dir.path().join("disk.vmdk");
1161 std::fs::File::create(&desc_path)
1162 .unwrap()
1163 .write_all(desc.as_bytes())
1164 .unwrap();
1165 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("deps");
1166 let names: Vec<String> = deps
1167 .iter()
1168 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
1169 .collect();
1170 assert_eq!(names, vec!["disk-s001.vmdk", "disk-s002.vmdk"]);
1171 }
1172
1173 #[test]
1174 fn extent_dependencies_empty_for_self_contained_binary() {
1175 use std::io::Write as _;
1177 let dir = tempfile::tempdir().unwrap();
1178 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1179 let path = dir.path().join("mono.vmdk");
1180 std::fs::File::create(&path)
1181 .unwrap()
1182 .write_all(&vmdk)
1183 .unwrap();
1184 let deps = VmdkFileReader::extent_dependencies(&path).expect("deps");
1185 assert!(
1186 deps.is_empty(),
1187 "self-contained binary VMDK has no companions"
1188 );
1189 }
1190
1191 #[test]
1192 fn extent_dependencies_excludes_zero_extents() {
1193 use std::io::Write as _;
1195 let dir = tempfile::tempdir().unwrap();
1196 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW 2048 ZERO\nRW 2048 FLAT \"real-f001.vmdk\" 0\n";
1197 let desc_path = dir.path().join("disk.vmdk");
1198 std::fs::File::create(&desc_path)
1199 .unwrap()
1200 .write_all(desc.as_bytes())
1201 .unwrap();
1202 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("deps");
1203 let names: Vec<String> = deps
1204 .iter()
1205 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
1206 .collect();
1207 assert_eq!(
1208 names,
1209 vec!["real-f001.vmdk"],
1210 "ZERO extent contributes no file"
1211 );
1212 }
1213
1214 #[test]
1215 fn extent_dependencies_skips_empty_sparse_filename() {
1216 use std::io::Write as _;
1218 let dir = tempfile::tempdir().unwrap();
1219 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"twoGbMaxExtentSparse\"\nRW 8 SPARSE \"\"\nRW 8 SPARSE \"real-s001.vmdk\"\n";
1220 let desc_path = dir.path().join("disk.vmdk");
1221 std::fs::File::create(&desc_path)
1222 .unwrap()
1223 .write_all(desc.as_bytes())
1224 .unwrap();
1225 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("deps");
1226 let names: Vec<String> = deps
1227 .iter()
1228 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
1229 .collect();
1230 assert_eq!(
1231 names,
1232 vec!["real-s001.vmdk"],
1233 "empty-filename sparse extent skipped"
1234 );
1235 }
1236
1237 #[test]
1240 fn grain_size_zero_rejected() {
1241 let img = vmdk_header_bytes(8, 0, 512);
1242 assert!(VmdkReader::open(Cursor::new(img)).is_err());
1243 }
1244
1245 #[test]
1246 fn num_gtes_per_gt_zero_rejected() {
1247 let img = vmdk_header_bytes(8, 8, 0);
1248 assert!(VmdkReader::open(Cursor::new(img)).is_err());
1249 }
1250
1251 #[test]
1252 fn open_empty_file_returns_err() {
1253 assert!(VmdkReader::open(Cursor::new(vec![])).is_err());
1254 }
1255
1256 #[test]
1257 fn open_non_vmdk_file_returns_err() {
1258 assert!(VmdkReader::open(Cursor::new(b"this is not a vmdk file at all".to_vec())).is_err());
1259 }
1260
1261 #[test]
1262 fn sparse_vmdk_virtual_disk_size() {
1263 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1264 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1265 assert_eq!(reader.virtual_disk_size(), GRAIN_SIZE_BYTES as u64);
1266 }
1267
1268 #[test]
1269 fn sparse_vmdk_read_returns_sector_data() {
1270 let mut data = vec![0u8; 512];
1271 data[42] = 0xDE;
1272 data[43] = 0xAD;
1273 let vmdk = test_sparse_vmdk(&data);
1274 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1275 let mut buf = vec![0u8; 512];
1276 reader.read_exact(&mut buf).expect("read");
1277 assert_eq!(buf[42], 0xDE);
1278 assert_eq!(buf[43], 0xAD);
1279 }
1280
1281 #[test]
1282 fn seek_and_read_at_offset() {
1283 let mut data = vec![0u8; GRAIN_SIZE_BYTES];
1284 data[100] = 0xBE;
1285 data[101] = 0xEF;
1286 let vmdk = test_sparse_vmdk(&data);
1287 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1288 reader.seek(SeekFrom::Start(100)).expect("seek");
1289 let mut buf = [0u8; 2];
1290 reader.read_exact(&mut buf).expect("read");
1291 assert_eq!(buf, [0xBE, 0xEF]);
1292 }
1293
1294 #[test]
1295 fn vmdk_reader_is_send() {
1296 fn assert_send<T: Send>() {}
1297 assert_send::<VmdkReader<Cursor<Vec<u8>>>>();
1298 }
1299
1300 #[test]
1301 fn stream_opt_gd_at_end_opens_correctly() {
1302 let vmdk = gd_at_end_stream_opt_vmdk();
1303 let reader = VmdkReader::open(Cursor::new(vmdk))
1304 .expect("streamOptimized GD_AT_END must open via footer lookup");
1305 assert_eq!(reader.virtual_disk_size(), 1_048_576);
1306 assert_eq!(reader.disk_type(), "streamOptimized");
1307 }
1308
1309 #[test]
1310 fn stream_opt_gd_at_end_reads_zeros() {
1311 let vmdk = gd_at_end_stream_opt_vmdk();
1312 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open GD_AT_END vmdk");
1313 let mut buf = [0xFFu8; 512];
1314 reader.read_exact(&mut buf).expect("read sector 0");
1315 assert_eq!(buf, [0u8; 512]);
1316 }
1317
1318 proptest::proptest! {
1319 #[test]
1320 fn open_never_panics_on_arbitrary_bytes(
1321 bytes in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..8192)
1322 ) {
1323 let _ = VmdkReader::open(Cursor::new(bytes));
1324 }
1325
1326 #[test]
1327 fn open_never_panics_on_valid_magic_plus_garbage(
1328 suffix in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..8192)
1329 ) {
1330 let mut bytes = vec![0u8; 8];
1331 bytes[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1332 bytes[4..8].copy_from_slice(&1u32.to_le_bytes());
1333 bytes.extend_from_slice(&suffix);
1334 let _ = VmdkReader::open(Cursor::new(bytes));
1335 }
1336 }
1337
1338 #[test]
1343 fn vmfs_flat_extent_descriptor_opens_via_open_path() {
1344 use std::io::Write as _;
1347 let dir = tempfile::tempdir().unwrap();
1348 let raw_path = dir.path().join("disk.vmdk");
1349 std::fs::File::create(&raw_path)
1350 .unwrap()
1351 .write_all(&vec![0u8; 512])
1352 .unwrap();
1353 let desc = format!(
1354 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"vmfs\"\nRW 1 VMFS \"{}\"\n",
1355 raw_path.file_name().unwrap().to_string_lossy()
1356 );
1357 let desc_path = dir.path().join("disk_desc.vmdk");
1358 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1359 let result = VmdkFileReader::open_path(&desc_path);
1360 result.expect("vmfs descriptor with VMFS extent must open");
1361 }
1362
1363 #[test]
1364 fn vmfssparse_extent_descriptor_opens_as_cowd() {
1365 use std::io::Write as _;
1367 let dir = tempfile::tempdir().unwrap();
1368 let cowd_bytes = testutil::test_cowd_vmdk(&[0u8; 512]);
1369 let cowd_path = dir.path().join("disk-delta.vmdk");
1370 std::fs::File::create(&cowd_path)
1371 .unwrap()
1372 .write_all(&cowd_bytes)
1373 .unwrap();
1374 let desc = format!(
1375 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"vmfsSparse\"\nRW 8 VMFSSPARSE \"{}\"\n",
1376 cowd_path.file_name().unwrap().to_string_lossy()
1377 );
1378 let desc_path = dir.path().join("desc.vmdk");
1379 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1380 let result = VmdkFileReader::open_path(&desc_path);
1381 result.expect("vmfsSparse/VMFSSPARSE descriptor must open");
1382 }
1383
1384 #[test]
1387 fn sesparse_vmdk_opens_successfully() {
1388 let se = test_sesparse_vmdk(&[0u8; 512]);
1389 VmdkReader::open(Cursor::new(se)).expect("seSparse VMDK must open");
1390 }
1391
1392 #[test]
1393 fn sesparse_vmdk_disk_type_is_sesparse() {
1394 let se = test_sesparse_vmdk(&[0u8; 512]);
1395 let reader = VmdkReader::open(Cursor::new(se)).expect("open");
1396 assert_eq!(reader.disk_type(), "seSparse");
1397 }
1398
1399 fn qemu_img_available() -> bool {
1409 std::process::Command::new("qemu-img")
1410 .arg("--version")
1411 .output()
1412 .is_ok_and(|o| o.status.success())
1413 }
1414
1415 fn assert_reader_matches_qemu(
1418 extent_bytes: &[u8],
1419 create_type: &str,
1420 extent_kw: &str,
1421 capacity_sectors: u64,
1422 ) {
1423 use std::io::Write as _;
1424 let dir = tempfile::tempdir().unwrap();
1425 let extent_path = dir.path().join("disk-extent.vmdk");
1426 std::fs::File::create(&extent_path)
1427 .unwrap()
1428 .write_all(extent_bytes)
1429 .unwrap();
1430 let desc = format!(
1431 "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\n\
1432 createType=\"{create_type}\"\nRW {capacity_sectors} {extent_kw} \"disk-extent.vmdk\"\n"
1433 );
1434 let desc_path = dir.path().join("disk.vmdk");
1435 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1436
1437 let qemu_raw = dir.path().join("qemu.raw");
1439 let status = std::process::Command::new("qemu-img")
1440 .args(["convert", "-O", "raw"])
1441 .arg(&desc_path)
1442 .arg(&qemu_raw)
1443 .status()
1444 .expect("run qemu-img convert");
1445 assert!(
1446 status.success(),
1447 "qemu-img convert failed for {create_type}"
1448 );
1449 let qemu_bytes = std::fs::read(&qemu_raw).unwrap();
1450
1451 let mut reader = VmdkFileReader::open_path(&desc_path).expect("open_path");
1453 reader.seek(SeekFrom::Start(0)).unwrap();
1454 let mut mine = Vec::new();
1455 reader.read_to_end(&mut mine).unwrap();
1456
1457 assert_eq!(
1458 mine.len(),
1459 qemu_bytes.len(),
1460 "{create_type}: size mismatch (mine {} vs qemu {})",
1461 mine.len(),
1462 qemu_bytes.len()
1463 );
1464 assert!(
1465 mine == qemu_bytes,
1466 "{create_type}: byte mismatch vs qemu-img — reader disagrees with the independent oracle"
1467 );
1468 }
1469
1470 #[test]
1471 fn cowd_reader_matches_qemu_img() {
1472 if !qemu_img_available() {
1473 eprintln!("skipping: qemu-img not installed");
1474 return;
1475 }
1476 let pattern: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
1477 let cowd = test_cowd_vmdk(&pattern);
1478 assert_reader_matches_qemu(&cowd, "vmfsSparse", "VMFSSPARSE", 8);
1479 }
1480
1481 #[test]
1482 fn sesparse_reader_matches_qemu_img() {
1483 if !qemu_img_available() {
1484 eprintln!("skipping: qemu-img not installed");
1485 return;
1486 }
1487 let pattern: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
1488 let se = test_sesparse_vmdk(&pattern);
1489 assert_reader_matches_qemu(&se, "seSparse", "SESPARSE", 8);
1490 }
1491
1492 #[test]
1493 fn sesparse_vmdk_reads_grain_data() {
1494 let mut data = vec![0u8; 512];
1495 data[0] = 0x5E;
1496 data[1] = 0xA5;
1497 let se = test_sesparse_vmdk(&data);
1498 let mut reader = VmdkReader::open(Cursor::new(se)).expect("open seSparse");
1499 let mut buf = [0u8; 512];
1500 reader.read_exact(&mut buf).expect("read");
1501 assert_eq!(buf[0], 0x5E);
1502 assert_eq!(buf[1], 0xA5);
1503 }
1504
1505 #[test]
1506 fn sesparse_extent_descriptor_opens_via_open_path() {
1507 use std::io::Write as _;
1512 let dir = tempfile::tempdir().unwrap();
1513 let mut data = vec![0u8; 512];
1514 data[0] = 0x7E;
1515 let se_bytes = test_sesparse_vmdk(&data);
1516 let se_path = dir.path().join("disk-sesparse.vmdk");
1517 std::fs::File::create(&se_path)
1518 .unwrap()
1519 .write_all(&se_bytes)
1520 .unwrap();
1521 let desc = format!(
1522 "# Disk DescriptorFile\nversion=1\nCID=abcdef01\nparentCID=ffffffff\ncreateType=\"seSparse\"\nRW 8 SESPARSE \"{}\"\n",
1523 se_path.file_name().unwrap().to_string_lossy()
1524 );
1525 let desc_path = dir.path().join("disk.vmdk");
1526 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1527 let mut reader = VmdkFileReader::open_path(&desc_path)
1528 .expect("seSparse descriptor must open via open_path");
1529 assert_eq!(reader.disk_type(), "seSparse");
1530 let mut buf = [0u8; 1];
1531 reader.read_exact(&mut buf).expect("read grain 0");
1532 assert_eq!(
1533 buf[0], 0x7E,
1534 "must read seSparse grain data through the descriptor"
1535 );
1536 }
1537
1538 #[test]
1541 fn cowd_vmdk_opens_without_bad_magic_error() {
1542 let cowd = test_cowd_vmdk(&[0u8; 512]);
1543 let reader = VmdkReader::open(Cursor::new(cowd));
1544 reader.expect("COWD VMDK must open successfully");
1545 }
1546
1547 #[test]
1548 fn cowd_vmdk_reads_grain_data() {
1549 let mut data = vec![0u8; 512];
1550 data[0] = 0xC0;
1551 data[1] = 0xBE;
1552 let cowd = test_cowd_vmdk(&data);
1553 let mut reader = VmdkReader::open(Cursor::new(cowd)).expect("open COWD");
1554 let mut buf = [0u8; 512];
1555 reader.read_exact(&mut buf).expect("read");
1556 assert_eq!(buf[0], 0xC0, "COWD grain data byte 0");
1557 assert_eq!(buf[1], 0xBE, "COWD grain data byte 1");
1558 }
1559
1560 #[test]
1561 fn cowd_vmdk_virtual_disk_size() {
1562 let cowd = test_cowd_vmdk(&[0u8; 512]);
1563 let reader = VmdkReader::open(Cursor::new(cowd)).expect("open");
1564 assert_eq!(reader.virtual_disk_size(), 8 * 512);
1566 }
1567
1568 #[test]
1571 fn hash_all_zeros_disk_produces_known_sha256() {
1572 use std::io::Cursor;
1574 let vmdk = gd_at_end_stream_opt_vmdk();
1575 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1576 reader.seek(SeekFrom::Start(0)).expect("seek");
1577 let digest = reader.hash().expect("hash");
1578 assert_eq!(
1581 digest.sha256, "30e14955ebf1352266dc2ff8067e68104607e750abb9d3b36582b8af909fcb58",
1582 "SHA-256 of 1 MiB all-zeros"
1583 );
1584 assert_eq!(
1585 digest.md5, "b6d81b360a5672d80c27430f39153e2c",
1586 "MD5 of 1 MiB all-zeros (matches qemu-img reference)"
1587 );
1588 }
1589
1590 #[test]
1591 fn hash_produces_hex_strings_of_correct_length() {
1592 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1593 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1594 reader.seek(SeekFrom::Start(0)).expect("seek");
1595 let digest = reader.hash().expect("hash");
1596 assert_eq!(digest.sha256.len(), 64, "SHA-256 hex must be 64 chars");
1597 assert_eq!(digest.md5.len(), 32, "MD5 hex must be 32 chars");
1598 }
1599
1600 #[cfg(feature = "serde")]
1603 #[test]
1604 fn vmdk_info_serializes_to_json() {
1605 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1606 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1607 let info = reader.info();
1608 let json = serde_json::to_string(&info).expect("serialize VmdkInfo to JSON");
1609 assert!(
1610 json.contains("\"disk_type\""),
1611 "JSON must contain disk_type field"
1612 );
1613 assert!(
1614 json.contains("monolithicSparse"),
1615 "JSON must contain createType value"
1616 );
1617 let info2: VmdkInfo = serde_json::from_str(&json).expect("deserialize VmdkInfo from JSON");
1618 assert_eq!(info2.disk_type, info.disk_type);
1619 assert_eq!(info2.virtual_disk_size, info.virtual_disk_size);
1620 }
1621
1622 #[cfg(feature = "serde")]
1623 #[test]
1624 fn allocated_grain_serializes_to_json() {
1625 let grain = AllocatedGrain {
1626 start_lba: 128,
1627 sector_count: 8,
1628 };
1629 let json = serde_json::to_string(&grain).expect("serialize AllocatedGrain");
1630 assert!(json.contains("\"start_lba\""));
1631 assert!(json.contains("128"));
1632 let grain2: AllocatedGrain = serde_json::from_str(&json).expect("deserialize");
1633 assert_eq!(grain2, grain);
1634 }
1635
1636 #[test]
1639 fn gt_cache_grows_on_grain_read() {
1640 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1641 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1642 assert_eq!(reader.gt_cache_size(), 0, "cache starts empty");
1643 let mut buf = [0u8; 512];
1644 reader.read_exact(&mut buf).expect("read");
1645 assert_eq!(
1646 reader.gt_cache_size(),
1647 1,
1648 "one GT loaded after first grain read"
1649 );
1650 }
1651
1652 #[test]
1653 fn gt_cache_no_double_load_on_second_read_same_grain() {
1654 let vmdk = test_sparse_vmdk(&[0xABu8; 512]);
1655 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1656 let mut buf = [0u8; 512];
1657 reader.read_exact(&mut buf).expect("first read");
1658 let after_first = reader.gt_cache_size();
1659 reader.seek(SeekFrom::Start(0)).expect("seek back");
1660 reader.read_exact(&mut buf).expect("second read");
1661 assert_eq!(
1662 reader.gt_cache_size(),
1663 after_first,
1664 "cache must not grow on second read of same GT"
1665 );
1666 assert_eq!(buf[0], 0xAB, "data must still be correct");
1667 }
1668
1669 #[test]
1672 fn sparse_grain_is_not_allocated() {
1673 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1676 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1677 assert!(
1679 reader.is_allocated(0).expect("is_allocated lba=0"),
1680 "grain 0 must be allocated"
1681 );
1682 let grain_sectors = GRAIN_SIZE_BYTES as u64 / 512;
1684 assert!(
1685 !reader
1686 .is_allocated(grain_sectors)
1687 .expect("is_allocated lba=grain_sectors"),
1688 "grain 1 must be sparse"
1689 );
1690 }
1691
1692 #[test]
1693 fn lba_beyond_disk_is_not_allocated() {
1694 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1695 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1696 let beyond = reader.sector_count() + 1;
1697 assert!(
1698 !reader
1699 .is_allocated(beyond)
1700 .expect("is_allocated beyond end"),
1701 "LBA beyond virtual disk must be not-allocated"
1702 );
1703 }
1704
1705 #[test]
1706 fn iter_allocated_grains_yields_grain_zero() {
1707 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1708 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1709 let grains = reader
1710 .iter_allocated_grains()
1711 .expect("iter_allocated_grains");
1712 assert_eq!(grains.len(), 1, "only grain 0 is allocated");
1713 assert_eq!(grains[0].start_lba, 0);
1714 assert_eq!(grains[0].sector_count, GRAIN_SIZE_BYTES as u64 / 512);
1715 }
1716
1717 #[test]
1718 fn iter_allocated_grains_all_sparse_returns_empty() {
1719 let vmdk = gd_at_end_stream_opt_vmdk(); let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1721 let grains = reader
1722 .iter_allocated_grains()
1723 .expect("iter_allocated_grains");
1724 assert!(
1725 grains.is_empty(),
1726 "all-sparse VMDK must yield no allocated grains"
1727 );
1728 }
1729
1730 #[test]
1733 fn sector_count_is_virtual_size_over_512() {
1734 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1735 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1736 assert_eq!(reader.sector_count() * 512, reader.virtual_disk_size());
1737 }
1738
1739 #[test]
1740 fn descriptor_text_contains_create_type() {
1741 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1742 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1743 let text = reader.descriptor_text();
1744 assert!(
1745 text.contains("monolithicSparse"),
1746 "descriptor_text must contain createType; got: {text:?}"
1747 );
1748 }
1749
1750 #[test]
1751 fn info_disk_type_matches_disk_type_method() {
1752 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1753 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1754 let info = reader.info();
1755 assert_eq!(info.disk_type, reader.disk_type());
1756 }
1757
1758 #[test]
1759 fn info_virtual_disk_size_and_sector_count_consistent() {
1760 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1761 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1762 let info = reader.info();
1763 assert_eq!(info.virtual_disk_size, reader.virtual_disk_size());
1764 assert_eq!(info.sector_count * 512, info.virtual_disk_size);
1765 }
1766
1767 #[test]
1768 fn info_grain_size_bytes_is_sectors_times_512() {
1769 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1770 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1771 let info = reader.info();
1772 assert_eq!(info.grain_size_bytes, info.grain_size_sectors * 512);
1773 assert!(
1774 info.grain_size_sectors >= 8,
1775 "grain_size_sectors must meet VDF 1.1 minimum"
1776 );
1777 }
1778
1779 #[test]
1780 fn info_cid_parsed_from_descriptor() {
1781 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1783 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1784 let info = reader.info();
1785 assert_eq!(
1786 info.cid, 0xffff_fffe,
1787 "CID must be parsed from embedded descriptor"
1788 );
1789 assert_eq!(
1790 info.parent_cid, 0xffff_ffff,
1791 "parentCID must be 0xffffffff (no parent) for a base image"
1792 );
1793 }
1794
1795 #[test]
1796 fn info_version_is_one_for_monolithic_sparse() {
1797 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1798 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1799 let info = reader.info();
1800 assert_eq!(info.version, 1);
1801 assert!(!info.compressed);
1802 }
1803
1804 #[test]
1807 fn compressed_grain_oversized_data_size_returns_invaliddata() {
1808 let vmdk = compressed_vmdk_with_oversized_marker(4 * 1024 * 1024);
1809 let mut reader = VmdkReader::open(Cursor::new(vmdk))
1810 .expect("VMDK with oversized marker must open — error only on read");
1811 let mut buf = [0u8; 512];
1812 let err = reader
1813 .read(&mut buf)
1814 .expect_err("oversized data_size must return Err");
1815 assert_eq!(
1816 err.kind(),
1817 io::ErrorKind::InvalidData,
1818 "must return InvalidData from cap check, not UnexpectedEof from allocation attempt"
1819 );
1820 }
1821
1822 #[test]
1823 fn grain_size_below_spec_minimum_is_rejected() {
1824 let mut hdr = vec![0u8; 512];
1825 hdr[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1826 hdr[4..8].copy_from_slice(&1u32.to_le_bytes());
1827 hdr[12..20].copy_from_slice(&128u64.to_le_bytes()); hdr[20..28].copy_from_slice(&4u64.to_le_bytes()); hdr[44..48].copy_from_slice(&512u32.to_le_bytes()); let result = VmdkReader::open(Cursor::new(hdr));
1831 assert!(
1832 result.is_err(),
1833 "grain_size=4 is below VDF 1.1 minimum of 8 sectors; open must return Err"
1834 );
1835 }
1836
1837 proptest::proptest! {
1838 #[test]
1839 fn open_never_panics_on_stream_opt_magic_plus_garbage(
1840 suffix in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..8192)
1841 ) {
1842 let mut bytes = vec![0u8; 8];
1843 bytes[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1844 bytes[4..8].copy_from_slice(&3u32.to_le_bytes()); bytes.extend_from_slice(&suffix);
1846 let _ = VmdkReader::open(Cursor::new(bytes));
1847 }
1848 }
1849
1850 fn qemu_img() -> Option<&'static str> {
1853 [
1854 "/opt/homebrew/bin/qemu-img",
1855 "/usr/bin/qemu-img",
1856 "/usr/local/bin/qemu-img",
1857 ]
1858 .into_iter()
1859 .find(|p| std::path::Path::new(p).exists())
1860 }
1861
1862 #[test]
1863 fn reads_match_qemu_raw_convert() {
1864 use std::fs::File;
1865 let Some(qemu_img) = qemu_img() else {
1866 return;
1867 };
1868 let tmp = tempfile::tempdir().expect("tempdir");
1869 let size: usize = 1 << 20;
1870 let raw_data: Vec<u8> = (0..size).map(|i| (i ^ (i >> 8)) as u8).collect();
1871 let raw_path = tmp.path().join("source.raw");
1872 std::fs::write(&raw_path, &raw_data).expect("write raw");
1873 let vmdk_path = tmp.path().join("test.vmdk");
1874 let status = std::process::Command::new(qemu_img)
1875 .args([
1876 "convert",
1877 "-O",
1878 "vmdk",
1879 raw_path.to_str().expect("UTF-8 path"),
1880 vmdk_path.to_str().expect("UTF-8 path"),
1881 ])
1882 .status()
1883 .expect("spawn qemu-img");
1884 assert!(status.success(), "qemu-img convert failed");
1885 let file = File::open(&vmdk_path).expect("open vmdk file");
1886 let mut reader = VmdkReader::open(file).expect("open");
1887 assert_eq!(reader.virtual_disk_size(), size as u64);
1888 let grain = 512 * 128;
1889 for &offset in &[0usize, 511, grain, grain + 512, size - 512] {
1890 let len = 512.min(size - offset);
1891 let mut buf = vec![0u8; len];
1892 reader.seek(SeekFrom::Start(offset as u64)).expect("seek");
1893 reader.read_exact(&mut buf).expect("read");
1894 assert_eq!(
1895 buf,
1896 raw_data[offset..offset + len],
1897 "byte mismatch at {offset:#x}"
1898 );
1899 }
1900 }
1901
1902 #[test]
1903 fn corpus_dfvfs_ext2_vmdk_reads_match_qemu_raw_convert() {
1904 use std::fs::File;
1905 let Some(qemu_img) = qemu_img() else {
1906 return;
1907 };
1908 let corpus =
1909 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/dfvfs_ext2.vmdk");
1910 if !corpus.exists() {
1911 return;
1912 }
1913 let tmp = tempfile::tempdir().expect("tempdir");
1914 let raw_path = tmp.path().join("ext2.raw");
1915 let ok = std::process::Command::new(qemu_img)
1916 .args([
1917 "convert",
1918 "-O",
1919 "raw",
1920 corpus.to_str().expect("UTF-8 path"),
1921 raw_path.to_str().expect("UTF-8 path"),
1922 ])
1923 .status()
1924 .expect("spawn qemu-img")
1925 .success();
1926 assert!(ok, "qemu-img convert failed for dfvfs_ext2.vmdk");
1927 let ref_data = std::fs::read(&raw_path).expect("read reference raw");
1928 let file = File::open(&corpus).expect("open dfvfs_ext2.vmdk");
1929 let mut reader = VmdkReader::open(file).expect("open");
1930 assert_eq!(
1931 reader.virtual_disk_size(),
1932 ref_data.len() as u64,
1933 "virtual_disk_size must match qemu-img raw for dfvfs_ext2.vmdk"
1934 );
1935 let vsize = ref_data.len();
1936 let step = 4096usize;
1937 let mut offset = 0usize;
1938 while offset < vsize {
1939 let len = 512.min(vsize - offset);
1940 let mut buf = vec![0u8; len];
1941 reader.seek(SeekFrom::Start(offset as u64)).expect("seek");
1942 reader.read_exact(&mut buf).expect("read");
1943 assert_eq!(
1944 buf,
1945 ref_data[offset..offset + len],
1946 "byte mismatch at {offset:#x} in dfvfs_ext2.vmdk"
1947 );
1948 offset += step;
1949 }
1950 }
1951
1952 #[test]
1953 fn corpus_minimal_vmdk_reads_match_qemu_raw_convert() {
1954 use std::fs::File;
1955 let Some(qemu_img) = qemu_img() else {
1956 return;
1957 };
1958 let corpus =
1959 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/minimal.vmdk");
1960 if !corpus.exists() {
1961 return;
1962 }
1963 let tmp = tempfile::tempdir().expect("tempdir");
1964 let raw_path = tmp.path().join("minimal.raw");
1965 let ok = std::process::Command::new(qemu_img)
1966 .args([
1967 "convert",
1968 "-O",
1969 "raw",
1970 corpus.to_str().expect("UTF-8 path"),
1971 raw_path.to_str().expect("UTF-8 path"),
1972 ])
1973 .status()
1974 .expect("spawn qemu-img")
1975 .success();
1976 assert!(ok, "qemu-img convert failed");
1977 let ref_data = std::fs::read(&raw_path).expect("read raw");
1978 let file = File::open(&corpus).expect("open corpus vmdk");
1979 let mut reader = VmdkReader::open(file).expect("open");
1980 assert_eq!(reader.virtual_disk_size(), ref_data.len() as u64);
1981 let vsize = ref_data.len();
1982 let grain = 65536usize;
1983 for &offset in &[0usize, 511, grain, grain + 512, vsize - 512] {
1984 let len = 512.min(vsize - offset);
1985 let mut buf = vec![0u8; len];
1986 reader.seek(SeekFrom::Start(offset as u64)).expect("seek");
1987 reader.read_exact(&mut buf).expect("read");
1988 assert_eq!(
1989 buf,
1990 ref_data[offset..offset + len],
1991 "byte mismatch at {offset:#x}"
1992 );
1993 }
1994 }
1995
1996 #[test]
1999 fn sesparse_is_allocated_and_iter() {
2000 let mut data = vec![0u8; 512];
2001 data[0] = 0x9A;
2002 let se = test_sesparse_vmdk(&data);
2003 let mut r = VmdkReader::open(Cursor::new(se)).expect("open");
2004 assert!(r.is_allocated(0).expect("grain 0 allocated"));
2005 assert!(!r
2006 .is_allocated(10_000)
2007 .expect("out-of-bounds lba is unallocated"));
2008 let grains = r.iter_allocated_grains().expect("iter");
2009 assert_eq!(grains.len(), 1);
2010 assert_eq!(grains[0].start_lba, 0);
2011 }
2012
2013 #[test]
2014 fn sesparse_invalid_gd_marker_errors_on_is_allocated() {
2015 let mut se = test_sesparse_vmdk(&[0u8; 512]);
2017 let gd = 2 * 512;
2018 se[gd..gd + 8].copy_from_slice(&0x5000_0000_0000_0000u64.to_le_bytes());
2019 let mut r = VmdkReader::open(Cursor::new(se)).expect("open");
2020 let err = r.is_allocated(0).expect_err("invalid GD marker must error");
2021 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2022 }
2023
2024 #[test]
2025 fn sesparse_invalid_gd_marker_skipped_in_iter() {
2026 let mut se = test_sesparse_vmdk(&[0u8; 512]);
2027 let gd = 2 * 512;
2028 se[gd..gd + 8].copy_from_slice(&0x5000_0000_0000_0000u64.to_le_bytes());
2029 let mut r = VmdkReader::open(Cursor::new(se)).expect("open");
2030 assert!(r.iter_allocated_grains().expect("iter").is_empty());
2031 }
2032
2033 fn open_flat_descriptor(dir: &std::path::Path, data: &[u8]) -> VmdkFileReader {
2036 use std::io::Write as _;
2037 let sectors = data.len().div_ceil(512).max(1);
2038 let mut ext = vec![0u8; sectors * 512];
2039 ext[..data.len()].copy_from_slice(data);
2040 std::fs::File::create(dir.join("disk-f001.vmdk"))
2041 .unwrap()
2042 .write_all(&ext)
2043 .unwrap();
2044 let desc = format!(
2045 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW {sectors} FLAT \"disk-f001.vmdk\" 0\n"
2046 );
2047 let desc_path = dir.join("disk.vmdk");
2048 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2049 VmdkFileReader::open_path(&desc_path).expect("open flat")
2050 }
2051
2052 #[test]
2053 fn flat_is_allocated_and_iter() {
2054 let dir = tempfile::tempdir().unwrap();
2055 let mut r = open_flat_descriptor(dir.path(), &[1u8; 1024]);
2056 assert!(r.is_allocated(0).expect("flat lba 0 allocated"));
2058 assert!(r.is_allocated(1).expect("flat lba 1 allocated"));
2059 assert!(!r.is_allocated(10_000).expect("oob unallocated"));
2060 let grains = r.iter_allocated_grains().expect("iter");
2062 assert_eq!(grains.len(), 1);
2063 assert_eq!(grains[0].start_lba, 0);
2064 assert_eq!(grains[0].sector_count, 2);
2065 }
2066
2067 #[test]
2068 fn sesparse_sparse_grain_directory_entry_reads_zero() {
2069 let mut se = test_sesparse_vmdk(&[0xAB; 512]);
2072 let cap = (sesparse::SE_GTES_PER_GT + 1) * 8; se[16..24].copy_from_slice(&cap.to_le_bytes()); let mut r = VmdkReader::open(Cursor::new(se)).expect("open");
2075 let lba = sesparse::SE_GTES_PER_GT * 8; assert!(!r.is_allocated(lba).expect("is_allocated"));
2077 assert_eq!(r.iter_allocated_grains().expect("iter").len(), 1);
2078 r.seek(SeekFrom::Start(lba * 512)).expect("seek");
2079 let mut buf = [0xFFu8; 512];
2080 r.read_exact(&mut buf).expect("read");
2081 assert_eq!(buf, [0u8; 512]);
2082 }
2083
2084 #[test]
2085 fn grain_location_and_grain_size_on_flat_reader() {
2086 let dir = tempfile::tempdir().unwrap();
2087 let mut r = open_flat_descriptor(dir.path(), &[1u8; 1024]);
2088 assert!(matches!(
2091 r.grain_location(0).expect("loc"),
2092 crate::read::GrainLookup::Sparse
2093 ));
2094 assert_eq!(r.sparse_grain_size_bytes(), 0);
2095 }
2096
2097 #[test]
2100 fn cid_and_parent_cid_accessors() {
2101 let vmdk = test_sparse_vmdk(&[0u8; 512]);
2102 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2103 assert_eq!(r.cid(), 0xffff_fffe); assert_eq!(r.parent_cid(), 0xffff_ffff);
2105 }
2106
2107 #[test]
2108 fn disk_database_accessor_and_info() {
2109 let desc = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\nddb.adapterType = \"lsilogic\"\nddb.geometry.cylinders = \"1024\"\nddb.geometry.heads = \"16\"\nddb.geometry.sectors = \"63\"\nddb.virtualHWVersion = \"13\"\nddb.thinProvisioned = \"1\"\n";
2110 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2111 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2112 let db = r.disk_database();
2113 assert_eq!(db.adapter_type.as_deref(), Some("lsilogic"));
2114 assert_eq!(db.virtual_hw_version.as_deref(), Some("13"));
2115 assert_eq!(db.thin_provisioned, Some(true));
2116 assert_eq!(db.geometry.unwrap().chs_sectors(), 1024 * 16 * 63);
2117 assert_eq!(r.info().disk_database, db);
2119 }
2120
2121 #[test]
2122 fn disk_database_empty_for_descriptorless_image() {
2123 let vmdk = test_sparse_vmdk(&[0u8; 512]); let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2125 assert!(r.disk_database().is_empty());
2126 }
2127
2128 #[test]
2129 fn change_track_path_reference() {
2130 let desc = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\nchangeTrackPath=\"disk-ctk.vmdk\"\n";
2131 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2132 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2133 assert_eq!(r.change_track_path().as_deref(), Some("disk-ctk.vmdk"));
2134 }
2135
2136 #[test]
2137 fn change_track_path_absent() {
2138 let vmdk = test_sparse_vmdk(&[0u8; 512]);
2139 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2140 assert_eq!(r.change_track_path(), None);
2141 }
2142
2143 #[test]
2144 fn effective_content_id_uses_long_cid_on_sentinel() {
2145 let desc = "# Disk DescriptorFile\nversion=1\nCID=fffffffe\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\nddb.longContentID = \"deadbeefcafef00d1122334455667788\"\n";
2147 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2148 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2149 assert_eq!(r.cid(), 0xffff_fffe);
2150 assert_eq!(r.effective_content_id(), "deadbeefcafef00d1122334455667788");
2151 }
2152
2153 #[test]
2154 fn effective_content_id_uses_short_cid_normally() {
2155 let desc = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n";
2156 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2157 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2158 assert_eq!(r.effective_content_id(), "12345678");
2159 }
2160
2161 #[test]
2162 fn rgd_fallback_recovers_grain_from_corrupt_primary_gd() {
2163 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2167 let gd_byte = 21 * 512; vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2169 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2170 r.enable_rgd_fallback();
2171 let mut buf = [0u8; 512];
2172 r.read_exact(&mut buf).expect("resilient read via RGD");
2173 assert_eq!(buf, [0xAB; 512], "grain recovered from redundant GD");
2174 }
2175
2176 #[test]
2177 fn corrupt_primary_gd_without_fallback_errors() {
2178 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2181 let gd_byte = 21 * 512;
2182 vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2183 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2184 let mut buf = [0u8; 512];
2185 assert!(
2186 r.read_exact(&mut buf).is_err(),
2187 "dangling primary GD pointer must error without fallback"
2188 );
2189 }
2190
2191 fn two_copy_vmdk_with_lost_primary_gte() -> Vec<u8> {
2197 const S: usize = 512;
2198 let mut hdr = vec![0u8; S];
2199 hdr[0..4].copy_from_slice(&header::MAGIC.to_le_bytes());
2200 hdr[4..8].copy_from_slice(&1u32.to_le_bytes());
2201 hdr[12..20].copy_from_slice(&8u64.to_le_bytes()); hdr[20..28].copy_from_slice(&8u64.to_le_bytes()); hdr[28..36].copy_from_slice(&1u64.to_le_bytes()); hdr[36..44].copy_from_slice(&20u64.to_le_bytes()); hdr[44..48].copy_from_slice(&512u32.to_le_bytes()); hdr[48..56].copy_from_slice(&22u64.to_le_bytes()); hdr[56..64].copy_from_slice(&21u64.to_le_bytes()); hdr[64..72].copy_from_slice(&31u64.to_le_bytes()); hdr[73..77].copy_from_slice(&[0x0A, 0x20, 0x0D, 0x0A]);
2210
2211 let mut desc = vec![0u8; 20 * S];
2212 let text = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n";
2213 desc[..text.len()].copy_from_slice(text.as_bytes());
2214
2215 let mut gd = vec![0u8; S];
2216 gd[0..4].copy_from_slice(&23u32.to_le_bytes()); let mut rgd = vec![0u8; S];
2218 rgd[0..4].copy_from_slice(&27u32.to_le_bytes()); let primary_gt = vec![0u8; 4 * S]; let mut redundant_gt = vec![0u8; 4 * S];
2222 redundant_gt[0..4].copy_from_slice(&31u32.to_le_bytes()); let grain = vec![0xABu8; 8 * S];
2225
2226 let mut v = Vec::new();
2227 v.extend_from_slice(&hdr);
2228 v.extend_from_slice(&desc);
2229 v.extend_from_slice(&gd);
2230 v.extend_from_slice(&rgd);
2231 v.extend_from_slice(&primary_gt);
2232 v.extend_from_slice(&redundant_gt);
2233 v.extend_from_slice(&grain);
2234 v
2235 }
2236
2237 #[test]
2238 fn rgd_fallback_recovers_grain_from_lost_primary_gte() {
2239 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2240 let mut r = VmdkReader::open(Cursor::new(vmdk.clone())).expect("open");
2242 let mut buf = [0xFFu8; 512];
2243 r.read_exact(&mut buf).expect("read");
2244 assert_eq!(
2245 buf, [0u8; 512],
2246 "lost primary GTE reads sparse without recovery"
2247 );
2248 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2250 r.enable_rgd_fallback();
2251 let mut buf = [0u8; 512];
2252 r.read_exact(&mut buf).expect("read");
2253 assert_eq!(buf, [0xAB; 512], "grain recovered from redundant GT entry");
2254 }
2255
2256 #[test]
2257 fn iter_allocated_grains_recovers_via_rgd() {
2258 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2261 let gd_byte = 21 * 512;
2262 vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2263 {
2264 let mut r = VmdkReader::open(Cursor::new(vmdk.clone())).expect("open");
2265 assert!(
2266 r.iter_allocated_grains().is_err(),
2267 "dangling primary GD pointer errors the scan without fallback"
2268 );
2269 }
2270 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2271 r.enable_rgd_fallback();
2272 let grains = r
2273 .iter_allocated_grains()
2274 .expect("allocation map recovered via RGD");
2275 assert_eq!(grains.len(), 1);
2276 assert_eq!(grains[0].start_lba, 0);
2277 }
2278
2279 #[test]
2280 fn iter_allocated_grains_recovers_lost_primary_gte() {
2281 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2284 {
2285 let mut r = VmdkReader::open(Cursor::new(vmdk.clone())).expect("open");
2286 assert_eq!(
2287 r.iter_allocated_grains().expect("scan").len(),
2288 0,
2289 "lost primary GTE is not listed without recovery"
2290 );
2291 }
2292 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2293 r.enable_rgd_fallback();
2294 let grains = r.iter_allocated_grains().expect("scan");
2295 assert_eq!(grains.len(), 1, "lost GTE recovered from redundant GT");
2296 assert_eq!(grains[0].start_lba, 0);
2297 }
2298
2299 #[test]
2300 fn rgd_recovery_count_tracks_pointer_recovery() {
2301 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2303 let gd_byte = 21 * 512;
2304 vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2305 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2306 r.enable_rgd_fallback();
2307 assert_eq!(r.rgd_recovery_count(), 0);
2308 let mut buf = [0u8; 512];
2309 r.read_exact(&mut buf).expect("read");
2310 assert_eq!(
2311 r.rgd_recovery_count(),
2312 1,
2313 "one grain recovered via RGD pointer"
2314 );
2315 }
2316
2317 #[test]
2318 fn rgd_recovery_count_tracks_entry_recovery() {
2319 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2321 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2322 r.enable_rgd_fallback();
2323 let mut buf = [0u8; 512];
2324 r.read_exact(&mut buf).expect("read");
2325 assert_eq!(
2326 r.rgd_recovery_count(),
2327 1,
2328 "one grain recovered via RGD entry"
2329 );
2330 }
2331
2332 #[test]
2333 fn rgd_recovery_count_zero_on_healthy_image() {
2334 let vmdk = test_sparse_vmdk(&[0xAB; 512]);
2335 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2336 r.enable_rgd_fallback();
2337 let mut buf = [0u8; 512];
2338 r.read_exact(&mut buf).expect("read");
2339 assert_eq!(
2340 r.rgd_recovery_count(),
2341 0,
2342 "healthy read uses the primary GD"
2343 );
2344 }
2345
2346 #[test]
2347 fn rgd_recovery_count_in_allocation_scan() {
2348 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2349 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2350 r.enable_rgd_fallback();
2351 let _ = r.iter_allocated_grains().expect("scan");
2352 assert_eq!(r.rgd_recovery_count(), 1, "scan counts the recovered grain");
2353 }
2354
2355 #[test]
2356 fn open_rejects_capacity_overflow() {
2357 let mut vmdk = test_sparse_vmdk(&[0u8; 512]);
2359 vmdk[12..20].copy_from_slice(&u64::MAX.to_le_bytes());
2360 assert!(matches!(
2361 VmdkReader::open(Cursor::new(vmdk)),
2362 Err(VmdkError::GeometryOverflow { field: "capacity" })
2363 ));
2364 }
2365
2366 #[test]
2367 fn content_recovery_with_no_rgd_offset_reads_sparse() {
2368 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2371 vmdk[23 * 512..23 * 512 + 4].copy_from_slice(&0u32.to_le_bytes()); vmdk[48..56].copy_from_slice(&0u64.to_le_bytes()); let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2374 r.enable_rgd_fallback();
2375 let mut buf = [0xFFu8; 512];
2376 r.read_exact(&mut buf).expect("read");
2377 assert_eq!(buf, [0u8; 512]);
2378 }
2379
2380 #[test]
2381 fn fallback_with_out_of_bounds_rgd_offset_is_safe() {
2382 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2385 vmdk[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2386 vmdk[48..56].copy_from_slice(&9_999_999u64.to_le_bytes());
2387 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2388 r.enable_rgd_fallback();
2389 let _ = r.iter_allocated_grains();
2390 }
2391
2392 #[test]
2393 fn fallback_scan_with_rgd_gt_past_eof_lists_primary() {
2394 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2397 vmdk[22 * 512..22 * 512 + 4].copy_from_slice(&9_999_999u32.to_le_bytes());
2398 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2399 r.enable_rgd_fallback();
2400 let grains = r.iter_allocated_grains().expect("scan");
2401 assert_eq!(grains.len(), 1);
2402 }
2403
2404 #[test]
2405 fn content_recovery_with_rgd_gt_past_eof_reads_sparse() {
2406 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2409 vmdk[23 * 512..23 * 512 + 4].copy_from_slice(&0u32.to_le_bytes());
2410 vmdk[22 * 512..22 * 512 + 4].copy_from_slice(&9_999_999u32.to_le_bytes());
2411 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2412 r.enable_rgd_fallback();
2413 let mut buf = [0xFFu8; 512];
2414 r.read_exact(&mut buf).expect("read");
2415 assert_eq!(buf, [0u8; 512]);
2416 }
2417
2418 #[test]
2419 fn rgd_fallback_is_noop_on_healthy_image() {
2420 let vmdk = test_sparse_vmdk(&[0xAB; 512]);
2422 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2423 r.enable_rgd_fallback();
2424 let mut buf = [0u8; 512];
2425 r.read_exact(&mut buf).expect("read healthy image");
2426 assert_eq!(buf, [0xAB; 512]);
2427 }
2428
2429 #[test]
2430 fn info_on_sesparse() {
2431 let se = test_sesparse_vmdk(&[0u8; 512]);
2432 let r = VmdkReader::open(Cursor::new(se)).expect("open");
2433 let info = r.info();
2434 assert_eq!(info.disk_type, "seSparse");
2435 assert_eq!(info.grain_size_bytes, 8 * 512);
2436 }
2437
2438 #[test]
2439 fn open_rejects_grain_directory_too_large() {
2440 let img = vmdk_header_bytes(1_000_000_000_000, 8, 512);
2442 assert!(matches!(
2443 VmdkReader::open(Cursor::new(img)),
2444 Err(VmdkError::FieldOutOfRange {
2445 field: "grain_directory",
2446 ..
2447 })
2448 ));
2449 }
2450
2451 fn sesparse_with_gte0(gte: u64) -> Vec<u8> {
2453 let mut se = test_sesparse_vmdk(&[0xABu8; 512]);
2454 let gt = 3 * 512; se[gt..gt + 8].copy_from_slice(>e.to_le_bytes());
2456 se
2457 }
2458
2459 #[test]
2460 fn sesparse_zero_unmapped_and_empty_gtes_read_as_zeros() {
2461 for gte in [0u64, 0x1000_0000_0000_0000, 0x2000_0000_0000_0000] {
2462 let mut r = VmdkReader::open(Cursor::new(sesparse_with_gte0(gte))).expect("open");
2463 r.seek(SeekFrom::Start(0)).unwrap();
2464 let mut buf = [0xFFu8; 512];
2465 r.read_exact(&mut buf).expect("read");
2466 assert_eq!(buf, [0u8; 512], "gte {gte:#x} must read as zeros");
2467 }
2468 }
2469
2470 #[test]
2471 fn sesparse_unsupported_type_nibble_errors_on_read() {
2472 let mut r =
2474 VmdkReader::open(Cursor::new(sesparse_with_gte0(0x4000_0000_0000_0000))).expect("open");
2475 let mut buf = [0u8; 512];
2476 let err = r.read(&mut buf).expect_err("unsupported nibble must error");
2477 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2478 }
2479
2480 #[test]
2481 fn custom_create_type_with_sparse_extent_opens() {
2482 use std::io::Write as _;
2483 let dir = tempfile::tempdir().unwrap();
2484 let ext = test_sparse_vmdk(&[0xC5u8; 512]);
2485 std::fs::File::create(dir.path().join("disk-s001.vmdk"))
2486 .unwrap()
2487 .write_all(&ext)
2488 .unwrap();
2489 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"custom\"\nRW 8 SPARSE \"disk-s001.vmdk\"\n";
2490 let desc_path = dir.path().join("disk.vmdk");
2491 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2492 let mut r = VmdkFileReader::open_path(&desc_path).expect("custom+sparse opens");
2493 let mut buf = [0u8; 1];
2494 r.read_exact(&mut buf).expect("read");
2495 assert_eq!(buf[0], 0xC5);
2496 }
2497
2498 #[test]
2499 fn custom_create_type_with_no_extents_errors() {
2500 let dir = tempfile::tempdir().unwrap();
2501 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"custom\"\n";
2502 let desc_path = dir.path().join("disk.vmdk");
2503 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2504 assert!(matches!(
2505 VmdkFileReader::open_path(&desc_path),
2506 Err(VmdkError::MalformedDescriptor(_))
2507 ));
2508 }
2509
2510 #[test]
2511 fn compressed_grain_decompressing_past_grain_size_is_refused() {
2512 use std::io::Read as _;
2516 let vmdk = crate::testutil::compressed_vmdk_with_bomb_grain(4 * 1024 * 1024);
2517 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2518 let mut buf = [0u8; 512];
2519 assert!(
2520 r.read(&mut buf).is_err(),
2521 "a grain that decompresses beyond its grain size must be refused"
2522 );
2523 }
2524
2525 #[test]
2526 fn descriptor_extent_path_cannot_escape_image_directory() {
2527 let outer = tempfile::tempdir().unwrap();
2530 std::fs::write(outer.path().join("secret.bin"), vec![0u8; 1024]).unwrap();
2531 let img = outer.path().join("img");
2532 std::fs::create_dir(&img).unwrap();
2533 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"twoGbMaxExtentFlat\"\nRW 2 FLAT \"../secret.bin\" 0\n";
2534 let desc_path = img.join("disk.vmdk");
2535 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2536 assert!(VmdkFileReader::open_path(&desc_path).is_err());
2538 }
2539
2540 #[test]
2541 fn custom_create_type_with_mixed_extents_errors() {
2542 let dir = tempfile::tempdir().unwrap();
2546 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"custom\"\nRW 2048 FLAT \"flat.bin\" 0\nRW 2048 SPARSE \"sparse.vmdk\"\n";
2547 let desc_path = dir.path().join("disk.vmdk");
2548 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2549 assert!(matches!(
2550 VmdkFileReader::open_path(&desc_path),
2551 Err(VmdkError::MalformedDescriptor(_))
2552 ));
2553 }
2554
2555 #[test]
2556 fn open_path_rejects_unknown_create_type() {
2557 let dir = tempfile::tempdir().unwrap();
2558 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"someFutureFormat\"\n";
2559 let desc_path = dir.path().join("disk.vmdk");
2560 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2561 assert!(matches!(
2562 VmdkFileReader::open_path(&desc_path),
2563 Err(VmdkError::UnsupportedDiskType(_))
2564 ));
2565 }
2566
2567 fn sparse_with_zero_gd_entry() -> Vec<u8> {
2571 const NGTE: u64 = 512;
2574 const GRAIN: u64 = 8;
2575 let capacity = (NGTE + 1) * GRAIN; let gd_sector = 1u64;
2577 let gt_sector = 2u64;
2578 let total_sectors = 10u64;
2579 let mut v = vec![0u8; total_sectors as usize * 512];
2580 v[0..4].copy_from_slice(&0x564D_444Bu32.to_le_bytes());
2581 v[4..8].copy_from_slice(&1u32.to_le_bytes());
2582 v[12..20].copy_from_slice(&capacity.to_le_bytes());
2583 v[20..28].copy_from_slice(&GRAIN.to_le_bytes());
2584 v[44..48].copy_from_slice(&(NGTE as u32).to_le_bytes());
2585 v[56..64].copy_from_slice(&gd_sector.to_le_bytes()); let gd = gd_sector as usize * 512;
2588 v[gd..gd + 4].copy_from_slice(&(gt_sector as u32).to_le_bytes());
2589 v
2591 }
2592
2593 #[test]
2594 fn sesparse_descriptor_without_extent_errors() {
2595 use std::io::Write as _;
2596 let dir = tempfile::tempdir().unwrap();
2597 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"seSparse\"\n";
2598 let p = dir.path().join("disk.vmdk");
2599 std::fs::File::create(&p)
2600 .unwrap()
2601 .write_all(desc.as_bytes())
2602 .unwrap();
2603 assert!(matches!(
2604 VmdkFileReader::open_path(&p),
2605 Err(VmdkError::MalformedDescriptor(_))
2606 ));
2607 }
2608
2609 #[test]
2610 fn sparse_empty_grain_table_entry_reads_zero_and_iterates_empty() {
2611 let vmdk = sparse_with_zero_gd_entry();
2612 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2613 let lba = 512 * 8; assert!(!r.is_allocated(lba).expect("is_allocated"));
2616 r.seek(SeekFrom::Start(lba * 512)).unwrap();
2618 let mut buf = [0xFFu8; 512];
2619 r.read_exact(&mut buf).unwrap();
2620 assert_eq!(buf, [0u8; 512]);
2621 assert!(r.iter_allocated_grains().expect("iter").is_empty());
2623 }
2624
2625 #[test]
2626 fn flat_zero_capacity_iter_is_empty() {
2627 use std::io::Write as _;
2629 let dir = tempfile::tempdir().unwrap();
2630 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW 0 ZERO\n";
2631 let p = dir.path().join("empty.vmdk");
2632 std::fs::File::create(&p)
2633 .unwrap()
2634 .write_all(desc.as_bytes())
2635 .unwrap();
2636 let mut r = VmdkFileReader::open_path(&p).expect("open empty flat");
2637 assert_eq!(r.virtual_disk_size(), 0);
2638 assert!(r.iter_allocated_grains().expect("iter").is_empty());
2639 }
2640}