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(test)]
1015mod tests {
1016 use super::*;
1017 use std::io::Cursor;
1018 use testutil::{
1019 compressed_vmdk_with_oversized_marker, gd_at_end_stream_opt_vmdk, test_cowd_vmdk,
1020 test_sesparse_vmdk, test_sparse_vmdk, GRAIN_SIZE_BYTES,
1021 };
1022
1023 fn vmdk_header_bytes(capacity_sectors: u64, grain_size: u64, num_gtes_per_gt: u32) -> Vec<u8> {
1024 let mut h = vec![0u8; 512];
1025 h[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1026 h[4..8].copy_from_slice(&1u32.to_le_bytes());
1027 h[12..20].copy_from_slice(&capacity_sectors.to_le_bytes());
1028 h[20..28].copy_from_slice(&grain_size.to_le_bytes());
1029 h[44..48].copy_from_slice(&num_gtes_per_gt.to_le_bytes());
1030 h
1031 }
1032
1033 #[test]
1036 fn header_version_2_zeroed_grain_opens() {
1037 let mut vmdk = test_sparse_vmdk(&[0u8; 512]);
1040 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))
1043 .expect("version=2 (zeroed-grain) monolithicSparse must open");
1044 }
1045
1046 #[test]
1047 fn zero_extent_type_reads_as_zeros() {
1048 use std::io::Write as _;
1051 let dir = tempfile::tempdir().unwrap();
1052 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW 2048 ZERO\n";
1053 let desc_path = dir.path().join("zero.vmdk");
1054 std::fs::File::create(&desc_path)
1055 .unwrap()
1056 .write_all(desc.as_bytes())
1057 .unwrap();
1058 let mut reader =
1059 VmdkFileReader::open_path(&desc_path).expect("descriptor with a ZERO extent must open");
1060 assert_eq!(
1061 reader.virtual_disk_size(),
1062 2048 * 512,
1063 "ZERO extent contributes its sector count"
1064 );
1065 reader.seek(SeekFrom::Start(0)).unwrap();
1066 let mut buf = [0xFFu8; 512];
1067 reader.read_exact(&mut buf).expect("read");
1068 assert_eq!(buf, [0u8; 512], "ZERO extent must read as zeros");
1069 }
1070
1071 fn assert_flat_create_type_reads(create_type: &str, extent_kw: &str, byte0: u8) {
1076 use std::io::Write as _;
1077 let dir = tempfile::tempdir().unwrap();
1078 let mut extent = vec![0u8; 1024];
1079 extent[0] = byte0;
1080 let extent_path = dir.path().join("disk-flat.vmdk");
1081 std::fs::File::create(&extent_path)
1082 .unwrap()
1083 .write_all(&extent)
1084 .unwrap();
1085 let offset = if extent_kw == "FLAT" { " 0" } else { "" };
1086 let desc = format!(
1087 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\n\
1088 createType=\"{create_type}\"\nRW 2 {extent_kw} \"disk-flat.vmdk\"{offset}\n"
1089 );
1090 let desc_path = dir.path().join("disk.vmdk");
1091 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1092 let mut reader = VmdkFileReader::open_path(&desc_path)
1093 .unwrap_or_else(|e| panic!("{create_type}/{extent_kw} must open: {e:?}"));
1094 let mut buf = [0u8; 1];
1095 reader.read_exact(&mut buf).expect("read");
1096 assert_eq!(
1097 buf[0], byte0,
1098 "{create_type}: must read the referenced extent"
1099 );
1100 }
1101
1102 #[test]
1103 fn custom_create_type_with_flat_extent_opens() {
1104 assert_flat_create_type_reads("custom", "FLAT", 0xC0);
1106 }
1107
1108 #[test]
1109 fn full_device_create_type_routes_to_flat() {
1110 assert_flat_create_type_reads("fullDevice", "FLAT", 0xFD);
1113 assert_flat_create_type_reads("partitionedDevice", "FLAT", 0xDE);
1114 }
1115
1116 #[test]
1117 fn vmfs_raw_rdm_create_types_route_to_flat() {
1118 assert_flat_create_type_reads("vmfsRaw", "VMFSRAW", 0x4A);
1121 assert_flat_create_type_reads("vmfsRawDeviceMap", "VMFSRAW", 0x4B);
1122 }
1123
1124 #[test]
1127 fn extent_dependencies_lists_flat_companion() {
1128 use std::io::Write as _;
1131 let dir = tempfile::tempdir().unwrap();
1132 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"twoGbMaxExtentFlat\"\nRW 2048 FLAT \"disk-f001.vmdk\" 0\n";
1133 let desc_path = dir.path().join("disk.vmdk");
1134 std::fs::File::create(&desc_path)
1135 .unwrap()
1136 .write_all(desc.as_bytes())
1137 .unwrap();
1138 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("extent_dependencies");
1139 assert_eq!(deps.len(), 1, "one companion extent");
1140 assert_eq!(
1141 deps[0].file_name().unwrap().to_string_lossy(),
1142 "disk-f001.vmdk"
1143 );
1144 assert_eq!(deps[0].parent().unwrap(), dir.path());
1146 }
1147
1148 #[test]
1149 fn extent_dependencies_lists_sparse_companions() {
1150 use std::io::Write as _;
1151 let dir = tempfile::tempdir().unwrap();
1152 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";
1153 let desc_path = dir.path().join("disk.vmdk");
1154 std::fs::File::create(&desc_path)
1155 .unwrap()
1156 .write_all(desc.as_bytes())
1157 .unwrap();
1158 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("deps");
1159 let names: Vec<String> = deps
1160 .iter()
1161 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
1162 .collect();
1163 assert_eq!(names, vec!["disk-s001.vmdk", "disk-s002.vmdk"]);
1164 }
1165
1166 #[test]
1167 fn extent_dependencies_empty_for_self_contained_binary() {
1168 use std::io::Write as _;
1170 let dir = tempfile::tempdir().unwrap();
1171 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1172 let path = dir.path().join("mono.vmdk");
1173 std::fs::File::create(&path)
1174 .unwrap()
1175 .write_all(&vmdk)
1176 .unwrap();
1177 let deps = VmdkFileReader::extent_dependencies(&path).expect("deps");
1178 assert!(
1179 deps.is_empty(),
1180 "self-contained binary VMDK has no companions"
1181 );
1182 }
1183
1184 #[test]
1185 fn extent_dependencies_excludes_zero_extents() {
1186 use std::io::Write as _;
1188 let dir = tempfile::tempdir().unwrap();
1189 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW 2048 ZERO\nRW 2048 FLAT \"real-f001.vmdk\" 0\n";
1190 let desc_path = dir.path().join("disk.vmdk");
1191 std::fs::File::create(&desc_path)
1192 .unwrap()
1193 .write_all(desc.as_bytes())
1194 .unwrap();
1195 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("deps");
1196 let names: Vec<String> = deps
1197 .iter()
1198 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
1199 .collect();
1200 assert_eq!(
1201 names,
1202 vec!["real-f001.vmdk"],
1203 "ZERO extent contributes no file"
1204 );
1205 }
1206
1207 #[test]
1208 fn extent_dependencies_skips_empty_sparse_filename() {
1209 use std::io::Write as _;
1211 let dir = tempfile::tempdir().unwrap();
1212 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"twoGbMaxExtentSparse\"\nRW 8 SPARSE \"\"\nRW 8 SPARSE \"real-s001.vmdk\"\n";
1213 let desc_path = dir.path().join("disk.vmdk");
1214 std::fs::File::create(&desc_path)
1215 .unwrap()
1216 .write_all(desc.as_bytes())
1217 .unwrap();
1218 let deps = VmdkFileReader::extent_dependencies(&desc_path).expect("deps");
1219 let names: Vec<String> = deps
1220 .iter()
1221 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
1222 .collect();
1223 assert_eq!(
1224 names,
1225 vec!["real-s001.vmdk"],
1226 "empty-filename sparse extent skipped"
1227 );
1228 }
1229
1230 #[test]
1233 fn grain_size_zero_rejected() {
1234 let img = vmdk_header_bytes(8, 0, 512);
1235 assert!(VmdkReader::open(Cursor::new(img)).is_err());
1236 }
1237
1238 #[test]
1239 fn num_gtes_per_gt_zero_rejected() {
1240 let img = vmdk_header_bytes(8, 8, 0);
1241 assert!(VmdkReader::open(Cursor::new(img)).is_err());
1242 }
1243
1244 #[test]
1245 fn open_empty_file_returns_err() {
1246 assert!(VmdkReader::open(Cursor::new(vec![])).is_err());
1247 }
1248
1249 #[test]
1250 fn open_non_vmdk_file_returns_err() {
1251 assert!(VmdkReader::open(Cursor::new(b"this is not a vmdk file at all".to_vec())).is_err());
1252 }
1253
1254 #[test]
1255 fn sparse_vmdk_virtual_disk_size() {
1256 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1257 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1258 assert_eq!(reader.virtual_disk_size(), GRAIN_SIZE_BYTES as u64);
1259 }
1260
1261 #[test]
1262 fn sparse_vmdk_read_returns_sector_data() {
1263 let mut data = vec![0u8; 512];
1264 data[42] = 0xDE;
1265 data[43] = 0xAD;
1266 let vmdk = test_sparse_vmdk(&data);
1267 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1268 let mut buf = vec![0u8; 512];
1269 reader.read_exact(&mut buf).expect("read");
1270 assert_eq!(buf[42], 0xDE);
1271 assert_eq!(buf[43], 0xAD);
1272 }
1273
1274 #[test]
1275 fn seek_and_read_at_offset() {
1276 let mut data = vec![0u8; GRAIN_SIZE_BYTES];
1277 data[100] = 0xBE;
1278 data[101] = 0xEF;
1279 let vmdk = test_sparse_vmdk(&data);
1280 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1281 reader.seek(SeekFrom::Start(100)).expect("seek");
1282 let mut buf = [0u8; 2];
1283 reader.read_exact(&mut buf).expect("read");
1284 assert_eq!(buf, [0xBE, 0xEF]);
1285 }
1286
1287 #[test]
1288 fn vmdk_reader_is_send() {
1289 fn assert_send<T: Send>() {}
1290 assert_send::<VmdkReader<Cursor<Vec<u8>>>>();
1291 }
1292
1293 #[test]
1294 fn stream_opt_gd_at_end_opens_correctly() {
1295 let vmdk = gd_at_end_stream_opt_vmdk();
1296 let reader = VmdkReader::open(Cursor::new(vmdk))
1297 .expect("streamOptimized GD_AT_END must open via footer lookup");
1298 assert_eq!(reader.virtual_disk_size(), 1_048_576);
1299 assert_eq!(reader.disk_type(), "streamOptimized");
1300 }
1301
1302 #[test]
1303 fn stream_opt_gd_at_end_reads_zeros() {
1304 let vmdk = gd_at_end_stream_opt_vmdk();
1305 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open GD_AT_END vmdk");
1306 let mut buf = [0xFFu8; 512];
1307 reader.read_exact(&mut buf).expect("read sector 0");
1308 assert_eq!(buf, [0u8; 512]);
1309 }
1310
1311 proptest::proptest! {
1312 #[test]
1313 fn open_never_panics_on_arbitrary_bytes(
1314 bytes in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..8192)
1315 ) {
1316 let _ = VmdkReader::open(Cursor::new(bytes));
1317 }
1318
1319 #[test]
1320 fn open_never_panics_on_valid_magic_plus_garbage(
1321 suffix in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..8192)
1322 ) {
1323 let mut bytes = vec![0u8; 8];
1324 bytes[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1325 bytes[4..8].copy_from_slice(&1u32.to_le_bytes());
1326 bytes.extend_from_slice(&suffix);
1327 let _ = VmdkReader::open(Cursor::new(bytes));
1328 }
1329 }
1330
1331 #[test]
1336 fn vmfs_flat_extent_descriptor_opens_via_open_path() {
1337 use std::io::Write as _;
1340 let dir = tempfile::tempdir().unwrap();
1341 let raw_path = dir.path().join("disk.vmdk");
1342 std::fs::File::create(&raw_path)
1343 .unwrap()
1344 .write_all(&vec![0u8; 512])
1345 .unwrap();
1346 let desc = format!(
1347 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"vmfs\"\nRW 1 VMFS \"{}\"\n",
1348 raw_path.file_name().unwrap().to_string_lossy()
1349 );
1350 let desc_path = dir.path().join("disk_desc.vmdk");
1351 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1352 let result = VmdkFileReader::open_path(&desc_path);
1353 result.expect("vmfs descriptor with VMFS extent must open");
1354 }
1355
1356 #[test]
1357 fn vmfssparse_extent_descriptor_opens_as_cowd() {
1358 use std::io::Write as _;
1360 let dir = tempfile::tempdir().unwrap();
1361 let cowd_bytes = testutil::test_cowd_vmdk(&[0u8; 512]);
1362 let cowd_path = dir.path().join("disk-delta.vmdk");
1363 std::fs::File::create(&cowd_path)
1364 .unwrap()
1365 .write_all(&cowd_bytes)
1366 .unwrap();
1367 let desc = format!(
1368 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"vmfsSparse\"\nRW 8 VMFSSPARSE \"{}\"\n",
1369 cowd_path.file_name().unwrap().to_string_lossy()
1370 );
1371 let desc_path = dir.path().join("desc.vmdk");
1372 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1373 let result = VmdkFileReader::open_path(&desc_path);
1374 result.expect("vmfsSparse/VMFSSPARSE descriptor must open");
1375 }
1376
1377 #[test]
1380 fn sesparse_vmdk_opens_successfully() {
1381 let se = test_sesparse_vmdk(&[0u8; 512]);
1382 VmdkReader::open(Cursor::new(se)).expect("seSparse VMDK must open");
1383 }
1384
1385 #[test]
1386 fn sesparse_vmdk_disk_type_is_sesparse() {
1387 let se = test_sesparse_vmdk(&[0u8; 512]);
1388 let reader = VmdkReader::open(Cursor::new(se)).expect("open");
1389 assert_eq!(reader.disk_type(), "seSparse");
1390 }
1391
1392 fn qemu_img_available() -> bool {
1402 std::process::Command::new("qemu-img")
1403 .arg("--version")
1404 .output()
1405 .is_ok_and(|o| o.status.success())
1406 }
1407
1408 fn assert_reader_matches_qemu(
1411 extent_bytes: &[u8],
1412 create_type: &str,
1413 extent_kw: &str,
1414 capacity_sectors: u64,
1415 ) {
1416 use std::io::Write as _;
1417 let dir = tempfile::tempdir().unwrap();
1418 let extent_path = dir.path().join("disk-extent.vmdk");
1419 std::fs::File::create(&extent_path)
1420 .unwrap()
1421 .write_all(extent_bytes)
1422 .unwrap();
1423 let desc = format!(
1424 "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\n\
1425 createType=\"{create_type}\"\nRW {capacity_sectors} {extent_kw} \"disk-extent.vmdk\"\n"
1426 );
1427 let desc_path = dir.path().join("disk.vmdk");
1428 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1429
1430 let qemu_raw = dir.path().join("qemu.raw");
1432 let status = std::process::Command::new("qemu-img")
1433 .args(["convert", "-O", "raw"])
1434 .arg(&desc_path)
1435 .arg(&qemu_raw)
1436 .status()
1437 .expect("run qemu-img convert");
1438 assert!(
1439 status.success(),
1440 "qemu-img convert failed for {create_type}"
1441 );
1442 let qemu_bytes = std::fs::read(&qemu_raw).unwrap();
1443
1444 let mut reader = VmdkFileReader::open_path(&desc_path).expect("open_path");
1446 reader.seek(SeekFrom::Start(0)).unwrap();
1447 let mut mine = Vec::new();
1448 reader.read_to_end(&mut mine).unwrap();
1449
1450 assert_eq!(
1451 mine.len(),
1452 qemu_bytes.len(),
1453 "{create_type}: size mismatch (mine {} vs qemu {})",
1454 mine.len(),
1455 qemu_bytes.len()
1456 );
1457 assert!(
1458 mine == qemu_bytes,
1459 "{create_type}: byte mismatch vs qemu-img — reader disagrees with the independent oracle"
1460 );
1461 }
1462
1463 #[test]
1464 fn cowd_reader_matches_qemu_img() {
1465 if !qemu_img_available() {
1466 eprintln!("skipping: qemu-img not installed");
1467 return;
1468 }
1469 let pattern: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
1470 let cowd = test_cowd_vmdk(&pattern);
1471 assert_reader_matches_qemu(&cowd, "vmfsSparse", "VMFSSPARSE", 8);
1472 }
1473
1474 #[test]
1475 fn sesparse_reader_matches_qemu_img() {
1476 if !qemu_img_available() {
1477 eprintln!("skipping: qemu-img not installed");
1478 return;
1479 }
1480 let pattern: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
1481 let se = test_sesparse_vmdk(&pattern);
1482 assert_reader_matches_qemu(&se, "seSparse", "SESPARSE", 8);
1483 }
1484
1485 #[test]
1486 fn sesparse_vmdk_reads_grain_data() {
1487 let mut data = vec![0u8; 512];
1488 data[0] = 0x5E;
1489 data[1] = 0xA5;
1490 let se = test_sesparse_vmdk(&data);
1491 let mut reader = VmdkReader::open(Cursor::new(se)).expect("open seSparse");
1492 let mut buf = [0u8; 512];
1493 reader.read_exact(&mut buf).expect("read");
1494 assert_eq!(buf[0], 0x5E);
1495 assert_eq!(buf[1], 0xA5);
1496 }
1497
1498 #[test]
1499 fn sesparse_extent_descriptor_opens_via_open_path() {
1500 use std::io::Write as _;
1505 let dir = tempfile::tempdir().unwrap();
1506 let mut data = vec![0u8; 512];
1507 data[0] = 0x7E;
1508 let se_bytes = test_sesparse_vmdk(&data);
1509 let se_path = dir.path().join("disk-sesparse.vmdk");
1510 std::fs::File::create(&se_path)
1511 .unwrap()
1512 .write_all(&se_bytes)
1513 .unwrap();
1514 let desc = format!(
1515 "# Disk DescriptorFile\nversion=1\nCID=abcdef01\nparentCID=ffffffff\ncreateType=\"seSparse\"\nRW 8 SESPARSE \"{}\"\n",
1516 se_path.file_name().unwrap().to_string_lossy()
1517 );
1518 let desc_path = dir.path().join("disk.vmdk");
1519 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
1520 let mut reader = VmdkFileReader::open_path(&desc_path)
1521 .expect("seSparse descriptor must open via open_path");
1522 assert_eq!(reader.disk_type(), "seSparse");
1523 let mut buf = [0u8; 1];
1524 reader.read_exact(&mut buf).expect("read grain 0");
1525 assert_eq!(
1526 buf[0], 0x7E,
1527 "must read seSparse grain data through the descriptor"
1528 );
1529 }
1530
1531 #[test]
1534 fn cowd_vmdk_opens_without_bad_magic_error() {
1535 let cowd = test_cowd_vmdk(&[0u8; 512]);
1536 let reader = VmdkReader::open(Cursor::new(cowd));
1537 reader.expect("COWD VMDK must open successfully");
1538 }
1539
1540 #[test]
1541 fn cowd_vmdk_reads_grain_data() {
1542 let mut data = vec![0u8; 512];
1543 data[0] = 0xC0;
1544 data[1] = 0xBE;
1545 let cowd = test_cowd_vmdk(&data);
1546 let mut reader = VmdkReader::open(Cursor::new(cowd)).expect("open COWD");
1547 let mut buf = [0u8; 512];
1548 reader.read_exact(&mut buf).expect("read");
1549 assert_eq!(buf[0], 0xC0, "COWD grain data byte 0");
1550 assert_eq!(buf[1], 0xBE, "COWD grain data byte 1");
1551 }
1552
1553 #[test]
1554 fn cowd_vmdk_virtual_disk_size() {
1555 let cowd = test_cowd_vmdk(&[0u8; 512]);
1556 let reader = VmdkReader::open(Cursor::new(cowd)).expect("open");
1557 assert_eq!(reader.virtual_disk_size(), 8 * 512);
1559 }
1560
1561 #[test]
1564 fn hash_all_zeros_disk_produces_known_sha256() {
1565 use std::io::Cursor;
1567 let vmdk = gd_at_end_stream_opt_vmdk();
1568 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1569 reader.seek(SeekFrom::Start(0)).expect("seek");
1570 let digest = reader.hash().expect("hash");
1571 assert_eq!(
1574 digest.sha256, "30e14955ebf1352266dc2ff8067e68104607e750abb9d3b36582b8af909fcb58",
1575 "SHA-256 of 1 MiB all-zeros"
1576 );
1577 assert_eq!(
1578 digest.md5, "b6d81b360a5672d80c27430f39153e2c",
1579 "MD5 of 1 MiB all-zeros (matches qemu-img reference)"
1580 );
1581 }
1582
1583 #[test]
1584 fn hash_produces_hex_strings_of_correct_length() {
1585 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1586 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1587 reader.seek(SeekFrom::Start(0)).expect("seek");
1588 let digest = reader.hash().expect("hash");
1589 assert_eq!(digest.sha256.len(), 64, "SHA-256 hex must be 64 chars");
1590 assert_eq!(digest.md5.len(), 32, "MD5 hex must be 32 chars");
1591 }
1592
1593 #[cfg(feature = "serde")]
1596 #[test]
1597 fn vmdk_info_serializes_to_json() {
1598 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1599 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1600 let info = reader.info();
1601 let json = serde_json::to_string(&info).expect("serialize VmdkInfo to JSON");
1602 assert!(
1603 json.contains("\"disk_type\""),
1604 "JSON must contain disk_type field"
1605 );
1606 assert!(
1607 json.contains("monolithicSparse"),
1608 "JSON must contain createType value"
1609 );
1610 let info2: VmdkInfo = serde_json::from_str(&json).expect("deserialize VmdkInfo from JSON");
1611 assert_eq!(info2.disk_type, info.disk_type);
1612 assert_eq!(info2.virtual_disk_size, info.virtual_disk_size);
1613 }
1614
1615 #[cfg(feature = "serde")]
1616 #[test]
1617 fn allocated_grain_serializes_to_json() {
1618 let grain = AllocatedGrain {
1619 start_lba: 128,
1620 sector_count: 8,
1621 };
1622 let json = serde_json::to_string(&grain).expect("serialize AllocatedGrain");
1623 assert!(json.contains("\"start_lba\""));
1624 assert!(json.contains("128"));
1625 let grain2: AllocatedGrain = serde_json::from_str(&json).expect("deserialize");
1626 assert_eq!(grain2, grain);
1627 }
1628
1629 #[test]
1632 fn gt_cache_grows_on_grain_read() {
1633 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1634 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1635 assert_eq!(reader.gt_cache_size(), 0, "cache starts empty");
1636 let mut buf = [0u8; 512];
1637 reader.read_exact(&mut buf).expect("read");
1638 assert_eq!(
1639 reader.gt_cache_size(),
1640 1,
1641 "one GT loaded after first grain read"
1642 );
1643 }
1644
1645 #[test]
1646 fn gt_cache_no_double_load_on_second_read_same_grain() {
1647 let vmdk = test_sparse_vmdk(&[0xABu8; 512]);
1648 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1649 let mut buf = [0u8; 512];
1650 reader.read_exact(&mut buf).expect("first read");
1651 let after_first = reader.gt_cache_size();
1652 reader.seek(SeekFrom::Start(0)).expect("seek back");
1653 reader.read_exact(&mut buf).expect("second read");
1654 assert_eq!(
1655 reader.gt_cache_size(),
1656 after_first,
1657 "cache must not grow on second read of same GT"
1658 );
1659 assert_eq!(buf[0], 0xAB, "data must still be correct");
1660 }
1661
1662 #[test]
1665 fn sparse_grain_is_not_allocated() {
1666 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1669 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1670 assert!(
1672 reader.is_allocated(0).expect("is_allocated lba=0"),
1673 "grain 0 must be allocated"
1674 );
1675 let grain_sectors = GRAIN_SIZE_BYTES as u64 / 512;
1677 assert!(
1678 !reader
1679 .is_allocated(grain_sectors)
1680 .expect("is_allocated lba=grain_sectors"),
1681 "grain 1 must be sparse"
1682 );
1683 }
1684
1685 #[test]
1686 fn lba_beyond_disk_is_not_allocated() {
1687 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1688 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1689 let beyond = reader.sector_count() + 1;
1690 assert!(
1691 !reader
1692 .is_allocated(beyond)
1693 .expect("is_allocated beyond end"),
1694 "LBA beyond virtual disk must be not-allocated"
1695 );
1696 }
1697
1698 #[test]
1699 fn iter_allocated_grains_yields_grain_zero() {
1700 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1701 let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1702 let grains = reader
1703 .iter_allocated_grains()
1704 .expect("iter_allocated_grains");
1705 assert_eq!(grains.len(), 1, "only grain 0 is allocated");
1706 assert_eq!(grains[0].start_lba, 0);
1707 assert_eq!(grains[0].sector_count, GRAIN_SIZE_BYTES as u64 / 512);
1708 }
1709
1710 #[test]
1711 fn iter_allocated_grains_all_sparse_returns_empty() {
1712 let vmdk = gd_at_end_stream_opt_vmdk(); let mut reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1714 let grains = reader
1715 .iter_allocated_grains()
1716 .expect("iter_allocated_grains");
1717 assert!(
1718 grains.is_empty(),
1719 "all-sparse VMDK must yield no allocated grains"
1720 );
1721 }
1722
1723 #[test]
1726 fn sector_count_is_virtual_size_over_512() {
1727 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1728 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1729 assert_eq!(reader.sector_count() * 512, reader.virtual_disk_size());
1730 }
1731
1732 #[test]
1733 fn descriptor_text_contains_create_type() {
1734 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1735 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1736 let text = reader.descriptor_text();
1737 assert!(
1738 text.contains("monolithicSparse"),
1739 "descriptor_text must contain createType; got: {text:?}"
1740 );
1741 }
1742
1743 #[test]
1744 fn info_disk_type_matches_disk_type_method() {
1745 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1746 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1747 let info = reader.info();
1748 assert_eq!(info.disk_type, reader.disk_type());
1749 }
1750
1751 #[test]
1752 fn info_virtual_disk_size_and_sector_count_consistent() {
1753 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1754 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1755 let info = reader.info();
1756 assert_eq!(info.virtual_disk_size, reader.virtual_disk_size());
1757 assert_eq!(info.sector_count * 512, info.virtual_disk_size);
1758 }
1759
1760 #[test]
1761 fn info_grain_size_bytes_is_sectors_times_512() {
1762 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1763 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1764 let info = reader.info();
1765 assert_eq!(info.grain_size_bytes, info.grain_size_sectors * 512);
1766 assert!(
1767 info.grain_size_sectors >= 8,
1768 "grain_size_sectors must meet VDF 1.1 minimum"
1769 );
1770 }
1771
1772 #[test]
1773 fn info_cid_parsed_from_descriptor() {
1774 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1776 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1777 let info = reader.info();
1778 assert_eq!(
1779 info.cid, 0xffff_fffe,
1780 "CID must be parsed from embedded descriptor"
1781 );
1782 assert_eq!(
1783 info.parent_cid, 0xffff_ffff,
1784 "parentCID must be 0xffffffff (no parent) for a base image"
1785 );
1786 }
1787
1788 #[test]
1789 fn info_version_is_one_for_monolithic_sparse() {
1790 let vmdk = test_sparse_vmdk(&[0u8; 512]);
1791 let reader = VmdkReader::open(Cursor::new(vmdk)).expect("open");
1792 let info = reader.info();
1793 assert_eq!(info.version, 1);
1794 assert!(!info.compressed);
1795 }
1796
1797 #[test]
1800 fn compressed_grain_oversized_data_size_returns_invaliddata() {
1801 let vmdk = compressed_vmdk_with_oversized_marker(4 * 1024 * 1024);
1802 let mut reader = VmdkReader::open(Cursor::new(vmdk))
1803 .expect("VMDK with oversized marker must open — error only on read");
1804 let mut buf = [0u8; 512];
1805 let err = reader
1806 .read(&mut buf)
1807 .expect_err("oversized data_size must return Err");
1808 assert_eq!(
1809 err.kind(),
1810 io::ErrorKind::InvalidData,
1811 "must return InvalidData from cap check, not UnexpectedEof from allocation attempt"
1812 );
1813 }
1814
1815 #[test]
1816 fn grain_size_below_spec_minimum_is_rejected() {
1817 let mut hdr = vec![0u8; 512];
1818 hdr[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1819 hdr[4..8].copy_from_slice(&1u32.to_le_bytes());
1820 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));
1824 assert!(
1825 result.is_err(),
1826 "grain_size=4 is below VDF 1.1 minimum of 8 sectors; open must return Err"
1827 );
1828 }
1829
1830 proptest::proptest! {
1831 #[test]
1832 fn open_never_panics_on_stream_opt_magic_plus_garbage(
1833 suffix in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..8192)
1834 ) {
1835 let mut bytes = vec![0u8; 8];
1836 bytes[0..4].copy_from_slice(&0x564D_444B_u32.to_le_bytes());
1837 bytes[4..8].copy_from_slice(&3u32.to_le_bytes()); bytes.extend_from_slice(&suffix);
1839 let _ = VmdkReader::open(Cursor::new(bytes));
1840 }
1841 }
1842
1843 fn qemu_img() -> Option<&'static str> {
1846 [
1847 "/opt/homebrew/bin/qemu-img",
1848 "/usr/bin/qemu-img",
1849 "/usr/local/bin/qemu-img",
1850 ]
1851 .into_iter()
1852 .find(|p| std::path::Path::new(p).exists())
1853 }
1854
1855 #[test]
1856 fn reads_match_qemu_raw_convert() {
1857 use std::fs::File;
1858 let Some(qemu_img) = qemu_img() else {
1859 return;
1860 };
1861 let tmp = tempfile::tempdir().expect("tempdir");
1862 let size: usize = 1 << 20;
1863 let raw_data: Vec<u8> = (0..size).map(|i| (i ^ (i >> 8)) as u8).collect();
1864 let raw_path = tmp.path().join("source.raw");
1865 std::fs::write(&raw_path, &raw_data).expect("write raw");
1866 let vmdk_path = tmp.path().join("test.vmdk");
1867 let status = std::process::Command::new(qemu_img)
1868 .args([
1869 "convert",
1870 "-O",
1871 "vmdk",
1872 raw_path.to_str().expect("UTF-8 path"),
1873 vmdk_path.to_str().expect("UTF-8 path"),
1874 ])
1875 .status()
1876 .expect("spawn qemu-img");
1877 assert!(status.success(), "qemu-img convert failed");
1878 let file = File::open(&vmdk_path).expect("open vmdk file");
1879 let mut reader = VmdkReader::open(file).expect("open");
1880 assert_eq!(reader.virtual_disk_size(), size as u64);
1881 let grain = 512 * 128;
1882 for &offset in &[0usize, 511, grain, grain + 512, size - 512] {
1883 let len = 512.min(size - offset);
1884 let mut buf = vec![0u8; len];
1885 reader.seek(SeekFrom::Start(offset as u64)).expect("seek");
1886 reader.read_exact(&mut buf).expect("read");
1887 assert_eq!(
1888 buf,
1889 raw_data[offset..offset + len],
1890 "byte mismatch at {offset:#x}"
1891 );
1892 }
1893 }
1894
1895 #[test]
1896 fn corpus_dfvfs_ext2_vmdk_reads_match_qemu_raw_convert() {
1897 use std::fs::File;
1898 let Some(qemu_img) = qemu_img() else {
1899 return;
1900 };
1901 let corpus =
1902 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/dfvfs_ext2.vmdk");
1903 if !corpus.exists() {
1904 return;
1905 }
1906 let tmp = tempfile::tempdir().expect("tempdir");
1907 let raw_path = tmp.path().join("ext2.raw");
1908 let ok = std::process::Command::new(qemu_img)
1909 .args([
1910 "convert",
1911 "-O",
1912 "raw",
1913 corpus.to_str().expect("UTF-8 path"),
1914 raw_path.to_str().expect("UTF-8 path"),
1915 ])
1916 .status()
1917 .expect("spawn qemu-img")
1918 .success();
1919 assert!(ok, "qemu-img convert failed for dfvfs_ext2.vmdk");
1920 let ref_data = std::fs::read(&raw_path).expect("read reference raw");
1921 let file = File::open(&corpus).expect("open dfvfs_ext2.vmdk");
1922 let mut reader = VmdkReader::open(file).expect("open");
1923 assert_eq!(
1924 reader.virtual_disk_size(),
1925 ref_data.len() as u64,
1926 "virtual_disk_size must match qemu-img raw for dfvfs_ext2.vmdk"
1927 );
1928 let vsize = ref_data.len();
1929 let step = 4096usize;
1930 let mut offset = 0usize;
1931 while offset < vsize {
1932 let len = 512.min(vsize - offset);
1933 let mut buf = vec![0u8; len];
1934 reader.seek(SeekFrom::Start(offset as u64)).expect("seek");
1935 reader.read_exact(&mut buf).expect("read");
1936 assert_eq!(
1937 buf,
1938 ref_data[offset..offset + len],
1939 "byte mismatch at {offset:#x} in dfvfs_ext2.vmdk"
1940 );
1941 offset += step;
1942 }
1943 }
1944
1945 #[test]
1946 fn corpus_minimal_vmdk_reads_match_qemu_raw_convert() {
1947 use std::fs::File;
1948 let Some(qemu_img) = qemu_img() else {
1949 return;
1950 };
1951 let corpus =
1952 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/minimal.vmdk");
1953 if !corpus.exists() {
1954 return;
1955 }
1956 let tmp = tempfile::tempdir().expect("tempdir");
1957 let raw_path = tmp.path().join("minimal.raw");
1958 let ok = std::process::Command::new(qemu_img)
1959 .args([
1960 "convert",
1961 "-O",
1962 "raw",
1963 corpus.to_str().expect("UTF-8 path"),
1964 raw_path.to_str().expect("UTF-8 path"),
1965 ])
1966 .status()
1967 .expect("spawn qemu-img")
1968 .success();
1969 assert!(ok, "qemu-img convert failed");
1970 let ref_data = std::fs::read(&raw_path).expect("read raw");
1971 let file = File::open(&corpus).expect("open corpus vmdk");
1972 let mut reader = VmdkReader::open(file).expect("open");
1973 assert_eq!(reader.virtual_disk_size(), ref_data.len() as u64);
1974 let vsize = ref_data.len();
1975 let grain = 65536usize;
1976 for &offset in &[0usize, 511, grain, grain + 512, vsize - 512] {
1977 let len = 512.min(vsize - offset);
1978 let mut buf = vec![0u8; len];
1979 reader.seek(SeekFrom::Start(offset as u64)).expect("seek");
1980 reader.read_exact(&mut buf).expect("read");
1981 assert_eq!(
1982 buf,
1983 ref_data[offset..offset + len],
1984 "byte mismatch at {offset:#x}"
1985 );
1986 }
1987 }
1988
1989 #[test]
1992 fn sesparse_is_allocated_and_iter() {
1993 let mut data = vec![0u8; 512];
1994 data[0] = 0x9A;
1995 let se = test_sesparse_vmdk(&data);
1996 let mut r = VmdkReader::open(Cursor::new(se)).expect("open");
1997 assert!(r.is_allocated(0).expect("grain 0 allocated"));
1998 assert!(!r
1999 .is_allocated(10_000)
2000 .expect("out-of-bounds lba is unallocated"));
2001 let grains = r.iter_allocated_grains().expect("iter");
2002 assert_eq!(grains.len(), 1);
2003 assert_eq!(grains[0].start_lba, 0);
2004 }
2005
2006 #[test]
2007 fn sesparse_invalid_gd_marker_errors_on_is_allocated() {
2008 let mut se = test_sesparse_vmdk(&[0u8; 512]);
2010 let gd = 2 * 512;
2011 se[gd..gd + 8].copy_from_slice(&0x5000_0000_0000_0000u64.to_le_bytes());
2012 let mut r = VmdkReader::open(Cursor::new(se)).expect("open");
2013 let err = r.is_allocated(0).expect_err("invalid GD marker must error");
2014 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2015 }
2016
2017 #[test]
2018 fn sesparse_invalid_gd_marker_skipped_in_iter() {
2019 let mut se = test_sesparse_vmdk(&[0u8; 512]);
2020 let gd = 2 * 512;
2021 se[gd..gd + 8].copy_from_slice(&0x5000_0000_0000_0000u64.to_le_bytes());
2022 let mut r = VmdkReader::open(Cursor::new(se)).expect("open");
2023 assert!(r.iter_allocated_grains().expect("iter").is_empty());
2024 }
2025
2026 fn open_flat_descriptor(dir: &std::path::Path, data: &[u8]) -> VmdkFileReader {
2029 use std::io::Write as _;
2030 let sectors = data.len().div_ceil(512).max(1);
2031 let mut ext = vec![0u8; sectors * 512];
2032 ext[..data.len()].copy_from_slice(data);
2033 std::fs::File::create(dir.join("disk-f001.vmdk"))
2034 .unwrap()
2035 .write_all(&ext)
2036 .unwrap();
2037 let desc = format!(
2038 "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW {sectors} FLAT \"disk-f001.vmdk\" 0\n"
2039 );
2040 let desc_path = dir.join("disk.vmdk");
2041 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2042 VmdkFileReader::open_path(&desc_path).expect("open flat")
2043 }
2044
2045 #[test]
2046 fn flat_is_allocated_and_iter() {
2047 let dir = tempfile::tempdir().unwrap();
2048 let mut r = open_flat_descriptor(dir.path(), &[1u8; 1024]);
2049 assert!(r.is_allocated(0).expect("flat lba 0 allocated"));
2051 assert!(r.is_allocated(1).expect("flat lba 1 allocated"));
2052 assert!(!r.is_allocated(10_000).expect("oob unallocated"));
2053 let grains = r.iter_allocated_grains().expect("iter");
2055 assert_eq!(grains.len(), 1);
2056 assert_eq!(grains[0].start_lba, 0);
2057 assert_eq!(grains[0].sector_count, 2);
2058 }
2059
2060 #[test]
2061 fn sesparse_sparse_grain_directory_entry_reads_zero() {
2062 let mut se = test_sesparse_vmdk(&[0xAB; 512]);
2065 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");
2068 let lba = sesparse::SE_GTES_PER_GT * 8; assert!(!r.is_allocated(lba).expect("is_allocated"));
2070 assert_eq!(r.iter_allocated_grains().expect("iter").len(), 1);
2071 r.seek(SeekFrom::Start(lba * 512)).expect("seek");
2072 let mut buf = [0xFFu8; 512];
2073 r.read_exact(&mut buf).expect("read");
2074 assert_eq!(buf, [0u8; 512]);
2075 }
2076
2077 #[test]
2078 fn grain_location_and_grain_size_on_flat_reader() {
2079 let dir = tempfile::tempdir().unwrap();
2080 let mut r = open_flat_descriptor(dir.path(), &[1u8; 1024]);
2081 assert!(matches!(
2084 r.grain_location(0).expect("loc"),
2085 crate::read::GrainLookup::Sparse
2086 ));
2087 assert_eq!(r.sparse_grain_size_bytes(), 0);
2088 }
2089
2090 #[test]
2093 fn cid_and_parent_cid_accessors() {
2094 let vmdk = test_sparse_vmdk(&[0u8; 512]);
2095 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2096 assert_eq!(r.cid(), 0xffff_fffe); assert_eq!(r.parent_cid(), 0xffff_ffff);
2098 }
2099
2100 #[test]
2101 fn disk_database_accessor_and_info() {
2102 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";
2103 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2104 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2105 let db = r.disk_database();
2106 assert_eq!(db.adapter_type.as_deref(), Some("lsilogic"));
2107 assert_eq!(db.virtual_hw_version.as_deref(), Some("13"));
2108 assert_eq!(db.thin_provisioned, Some(true));
2109 assert_eq!(db.geometry.unwrap().chs_sectors(), 1024 * 16 * 63);
2110 assert_eq!(r.info().disk_database, db);
2112 }
2113
2114 #[test]
2115 fn disk_database_empty_for_descriptorless_image() {
2116 let vmdk = test_sparse_vmdk(&[0u8; 512]); let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2118 assert!(r.disk_database().is_empty());
2119 }
2120
2121 #[test]
2122 fn change_track_path_reference() {
2123 let desc = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\nchangeTrackPath=\"disk-ctk.vmdk\"\n";
2124 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2125 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2126 assert_eq!(r.change_track_path().as_deref(), Some("disk-ctk.vmdk"));
2127 }
2128
2129 #[test]
2130 fn change_track_path_absent() {
2131 let vmdk = test_sparse_vmdk(&[0u8; 512]);
2132 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2133 assert_eq!(r.change_track_path(), None);
2134 }
2135
2136 #[test]
2137 fn effective_content_id_uses_long_cid_on_sentinel() {
2138 let desc = "# Disk DescriptorFile\nversion=1\nCID=fffffffe\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\nddb.longContentID = \"deadbeefcafef00d1122334455667788\"\n";
2140 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2141 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2142 assert_eq!(r.cid(), 0xffff_fffe);
2143 assert_eq!(r.effective_content_id(), "deadbeefcafef00d1122334455667788");
2144 }
2145
2146 #[test]
2147 fn effective_content_id_uses_short_cid_normally() {
2148 let desc = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n";
2149 let vmdk = testutil::test_sparse_vmdk_with_descriptor(&[0u8; 512], desc);
2150 let r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2151 assert_eq!(r.effective_content_id(), "12345678");
2152 }
2153
2154 #[test]
2155 fn rgd_fallback_recovers_grain_from_corrupt_primary_gd() {
2156 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2160 let gd_byte = 21 * 512; vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2162 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2163 r.enable_rgd_fallback();
2164 let mut buf = [0u8; 512];
2165 r.read_exact(&mut buf).expect("resilient read via RGD");
2166 assert_eq!(buf, [0xAB; 512], "grain recovered from redundant GD");
2167 }
2168
2169 #[test]
2170 fn corrupt_primary_gd_without_fallback_errors() {
2171 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2174 let gd_byte = 21 * 512;
2175 vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2176 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2177 let mut buf = [0u8; 512];
2178 assert!(
2179 r.read_exact(&mut buf).is_err(),
2180 "dangling primary GD pointer must error without fallback"
2181 );
2182 }
2183
2184 fn two_copy_vmdk_with_lost_primary_gte() -> Vec<u8> {
2190 const S: usize = 512;
2191 let mut hdr = vec![0u8; S];
2192 hdr[0..4].copy_from_slice(&header::MAGIC.to_le_bytes());
2193 hdr[4..8].copy_from_slice(&1u32.to_le_bytes());
2194 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]);
2203
2204 let mut desc = vec![0u8; 20 * S];
2205 let text = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n";
2206 desc[..text.len()].copy_from_slice(text.as_bytes());
2207
2208 let mut gd = vec![0u8; S];
2209 gd[0..4].copy_from_slice(&23u32.to_le_bytes()); let mut rgd = vec![0u8; S];
2211 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];
2215 redundant_gt[0..4].copy_from_slice(&31u32.to_le_bytes()); let grain = vec![0xABu8; 8 * S];
2218
2219 let mut v = Vec::new();
2220 v.extend_from_slice(&hdr);
2221 v.extend_from_slice(&desc);
2222 v.extend_from_slice(&gd);
2223 v.extend_from_slice(&rgd);
2224 v.extend_from_slice(&primary_gt);
2225 v.extend_from_slice(&redundant_gt);
2226 v.extend_from_slice(&grain);
2227 v
2228 }
2229
2230 #[test]
2231 fn rgd_fallback_recovers_grain_from_lost_primary_gte() {
2232 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2233 let mut r = VmdkReader::open(Cursor::new(vmdk.clone())).expect("open");
2235 let mut buf = [0xFFu8; 512];
2236 r.read_exact(&mut buf).expect("read");
2237 assert_eq!(
2238 buf, [0u8; 512],
2239 "lost primary GTE reads sparse without recovery"
2240 );
2241 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2243 r.enable_rgd_fallback();
2244 let mut buf = [0u8; 512];
2245 r.read_exact(&mut buf).expect("read");
2246 assert_eq!(buf, [0xAB; 512], "grain recovered from redundant GT entry");
2247 }
2248
2249 #[test]
2250 fn iter_allocated_grains_recovers_via_rgd() {
2251 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2254 let gd_byte = 21 * 512;
2255 vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2256 {
2257 let mut r = VmdkReader::open(Cursor::new(vmdk.clone())).expect("open");
2258 assert!(
2259 r.iter_allocated_grains().is_err(),
2260 "dangling primary GD pointer errors the scan without fallback"
2261 );
2262 }
2263 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2264 r.enable_rgd_fallback();
2265 let grains = r
2266 .iter_allocated_grains()
2267 .expect("allocation map recovered via RGD");
2268 assert_eq!(grains.len(), 1);
2269 assert_eq!(grains[0].start_lba, 0);
2270 }
2271
2272 #[test]
2273 fn iter_allocated_grains_recovers_lost_primary_gte() {
2274 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2277 {
2278 let mut r = VmdkReader::open(Cursor::new(vmdk.clone())).expect("open");
2279 assert_eq!(
2280 r.iter_allocated_grains().expect("scan").len(),
2281 0,
2282 "lost primary GTE is not listed without recovery"
2283 );
2284 }
2285 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2286 r.enable_rgd_fallback();
2287 let grains = r.iter_allocated_grains().expect("scan");
2288 assert_eq!(grains.len(), 1, "lost GTE recovered from redundant GT");
2289 assert_eq!(grains[0].start_lba, 0);
2290 }
2291
2292 #[test]
2293 fn rgd_recovery_count_tracks_pointer_recovery() {
2294 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2296 let gd_byte = 21 * 512;
2297 vmdk[gd_byte..gd_byte + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2298 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2299 r.enable_rgd_fallback();
2300 assert_eq!(r.rgd_recovery_count(), 0);
2301 let mut buf = [0u8; 512];
2302 r.read_exact(&mut buf).expect("read");
2303 assert_eq!(
2304 r.rgd_recovery_count(),
2305 1,
2306 "one grain recovered via RGD pointer"
2307 );
2308 }
2309
2310 #[test]
2311 fn rgd_recovery_count_tracks_entry_recovery() {
2312 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2314 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2315 r.enable_rgd_fallback();
2316 let mut buf = [0u8; 512];
2317 r.read_exact(&mut buf).expect("read");
2318 assert_eq!(
2319 r.rgd_recovery_count(),
2320 1,
2321 "one grain recovered via RGD entry"
2322 );
2323 }
2324
2325 #[test]
2326 fn rgd_recovery_count_zero_on_healthy_image() {
2327 let vmdk = test_sparse_vmdk(&[0xAB; 512]);
2328 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2329 r.enable_rgd_fallback();
2330 let mut buf = [0u8; 512];
2331 r.read_exact(&mut buf).expect("read");
2332 assert_eq!(
2333 r.rgd_recovery_count(),
2334 0,
2335 "healthy read uses the primary GD"
2336 );
2337 }
2338
2339 #[test]
2340 fn rgd_recovery_count_in_allocation_scan() {
2341 let vmdk = two_copy_vmdk_with_lost_primary_gte();
2342 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2343 r.enable_rgd_fallback();
2344 let _ = r.iter_allocated_grains().expect("scan");
2345 assert_eq!(r.rgd_recovery_count(), 1, "scan counts the recovered grain");
2346 }
2347
2348 #[test]
2349 fn open_rejects_capacity_overflow() {
2350 let mut vmdk = test_sparse_vmdk(&[0u8; 512]);
2352 vmdk[12..20].copy_from_slice(&u64::MAX.to_le_bytes());
2353 assert!(matches!(
2354 VmdkReader::open(Cursor::new(vmdk)),
2355 Err(VmdkError::GeometryOverflow { field: "capacity" })
2356 ));
2357 }
2358
2359 #[test]
2360 fn content_recovery_with_no_rgd_offset_reads_sparse() {
2361 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2364 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");
2367 r.enable_rgd_fallback();
2368 let mut buf = [0xFFu8; 512];
2369 r.read_exact(&mut buf).expect("read");
2370 assert_eq!(buf, [0u8; 512]);
2371 }
2372
2373 #[test]
2374 fn fallback_with_out_of_bounds_rgd_offset_is_safe() {
2375 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2378 vmdk[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
2379 vmdk[48..56].copy_from_slice(&9_999_999u64.to_le_bytes());
2380 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2381 r.enable_rgd_fallback();
2382 let _ = r.iter_allocated_grains();
2383 }
2384
2385 #[test]
2386 fn fallback_scan_with_rgd_gt_past_eof_lists_primary() {
2387 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2390 vmdk[22 * 512..22 * 512 + 4].copy_from_slice(&9_999_999u32.to_le_bytes());
2391 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2392 r.enable_rgd_fallback();
2393 let grains = r.iter_allocated_grains().expect("scan");
2394 assert_eq!(grains.len(), 1);
2395 }
2396
2397 #[test]
2398 fn content_recovery_with_rgd_gt_past_eof_reads_sparse() {
2399 let mut vmdk = test_sparse_vmdk(&[0xAB; 512]);
2402 vmdk[23 * 512..23 * 512 + 4].copy_from_slice(&0u32.to_le_bytes());
2403 vmdk[22 * 512..22 * 512 + 4].copy_from_slice(&9_999_999u32.to_le_bytes());
2404 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2405 r.enable_rgd_fallback();
2406 let mut buf = [0xFFu8; 512];
2407 r.read_exact(&mut buf).expect("read");
2408 assert_eq!(buf, [0u8; 512]);
2409 }
2410
2411 #[test]
2412 fn rgd_fallback_is_noop_on_healthy_image() {
2413 let vmdk = test_sparse_vmdk(&[0xAB; 512]);
2415 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2416 r.enable_rgd_fallback();
2417 let mut buf = [0u8; 512];
2418 r.read_exact(&mut buf).expect("read healthy image");
2419 assert_eq!(buf, [0xAB; 512]);
2420 }
2421
2422 #[test]
2423 fn info_on_sesparse() {
2424 let se = test_sesparse_vmdk(&[0u8; 512]);
2425 let r = VmdkReader::open(Cursor::new(se)).expect("open");
2426 let info = r.info();
2427 assert_eq!(info.disk_type, "seSparse");
2428 assert_eq!(info.grain_size_bytes, 8 * 512);
2429 }
2430
2431 #[test]
2432 fn open_rejects_grain_directory_too_large() {
2433 let img = vmdk_header_bytes(1_000_000_000_000, 8, 512);
2435 assert!(matches!(
2436 VmdkReader::open(Cursor::new(img)),
2437 Err(VmdkError::FieldOutOfRange {
2438 field: "grain_directory",
2439 ..
2440 })
2441 ));
2442 }
2443
2444 fn sesparse_with_gte0(gte: u64) -> Vec<u8> {
2446 let mut se = test_sesparse_vmdk(&[0xABu8; 512]);
2447 let gt = 3 * 512; se[gt..gt + 8].copy_from_slice(>e.to_le_bytes());
2449 se
2450 }
2451
2452 #[test]
2453 fn sesparse_zero_unmapped_and_empty_gtes_read_as_zeros() {
2454 for gte in [0u64, 0x1000_0000_0000_0000, 0x2000_0000_0000_0000] {
2455 let mut r = VmdkReader::open(Cursor::new(sesparse_with_gte0(gte))).expect("open");
2456 r.seek(SeekFrom::Start(0)).unwrap();
2457 let mut buf = [0xFFu8; 512];
2458 r.read_exact(&mut buf).expect("read");
2459 assert_eq!(buf, [0u8; 512], "gte {gte:#x} must read as zeros");
2460 }
2461 }
2462
2463 #[test]
2464 fn sesparse_unsupported_type_nibble_errors_on_read() {
2465 let mut r =
2467 VmdkReader::open(Cursor::new(sesparse_with_gte0(0x4000_0000_0000_0000))).expect("open");
2468 let mut buf = [0u8; 512];
2469 let err = r.read(&mut buf).expect_err("unsupported nibble must error");
2470 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2471 }
2472
2473 #[test]
2474 fn custom_create_type_with_sparse_extent_opens() {
2475 use std::io::Write as _;
2476 let dir = tempfile::tempdir().unwrap();
2477 let ext = test_sparse_vmdk(&[0xC5u8; 512]);
2478 std::fs::File::create(dir.path().join("disk-s001.vmdk"))
2479 .unwrap()
2480 .write_all(&ext)
2481 .unwrap();
2482 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"custom\"\nRW 8 SPARSE \"disk-s001.vmdk\"\n";
2483 let desc_path = dir.path().join("disk.vmdk");
2484 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2485 let mut r = VmdkFileReader::open_path(&desc_path).expect("custom+sparse opens");
2486 let mut buf = [0u8; 1];
2487 r.read_exact(&mut buf).expect("read");
2488 assert_eq!(buf[0], 0xC5);
2489 }
2490
2491 #[test]
2492 fn custom_create_type_with_no_extents_errors() {
2493 let dir = tempfile::tempdir().unwrap();
2494 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"custom\"\n";
2495 let desc_path = dir.path().join("disk.vmdk");
2496 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2497 assert!(matches!(
2498 VmdkFileReader::open_path(&desc_path),
2499 Err(VmdkError::MalformedDescriptor(_))
2500 ));
2501 }
2502
2503 #[test]
2504 fn compressed_grain_decompressing_past_grain_size_is_refused() {
2505 use std::io::Read as _;
2509 let vmdk = crate::testutil::compressed_vmdk_with_bomb_grain(4 * 1024 * 1024);
2510 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2511 let mut buf = [0u8; 512];
2512 assert!(
2513 r.read(&mut buf).is_err(),
2514 "a grain that decompresses beyond its grain size must be refused"
2515 );
2516 }
2517
2518 #[test]
2519 fn descriptor_extent_path_cannot_escape_image_directory() {
2520 let outer = tempfile::tempdir().unwrap();
2523 std::fs::write(outer.path().join("secret.bin"), vec![0u8; 1024]).unwrap();
2524 let img = outer.path().join("img");
2525 std::fs::create_dir(&img).unwrap();
2526 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"twoGbMaxExtentFlat\"\nRW 2 FLAT \"../secret.bin\" 0\n";
2527 let desc_path = img.join("disk.vmdk");
2528 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2529 assert!(VmdkFileReader::open_path(&desc_path).is_err());
2531 }
2532
2533 #[test]
2534 fn custom_create_type_with_mixed_extents_errors() {
2535 let dir = tempfile::tempdir().unwrap();
2539 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"custom\"\nRW 2048 FLAT \"flat.bin\" 0\nRW 2048 SPARSE \"sparse.vmdk\"\n";
2540 let desc_path = dir.path().join("disk.vmdk");
2541 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2542 assert!(matches!(
2543 VmdkFileReader::open_path(&desc_path),
2544 Err(VmdkError::MalformedDescriptor(_))
2545 ));
2546 }
2547
2548 #[test]
2549 fn open_path_rejects_unknown_create_type() {
2550 let dir = tempfile::tempdir().unwrap();
2551 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"someFutureFormat\"\n";
2552 let desc_path = dir.path().join("disk.vmdk");
2553 std::fs::write(&desc_path, desc.as_bytes()).unwrap();
2554 assert!(matches!(
2555 VmdkFileReader::open_path(&desc_path),
2556 Err(VmdkError::UnsupportedDiskType(_))
2557 ));
2558 }
2559
2560 fn sparse_with_zero_gd_entry() -> Vec<u8> {
2564 const NGTE: u64 = 512;
2567 const GRAIN: u64 = 8;
2568 let capacity = (NGTE + 1) * GRAIN; let gd_sector = 1u64;
2570 let gt_sector = 2u64;
2571 let total_sectors = 10u64;
2572 let mut v = vec![0u8; total_sectors as usize * 512];
2573 v[0..4].copy_from_slice(&0x564D_444Bu32.to_le_bytes());
2574 v[4..8].copy_from_slice(&1u32.to_le_bytes());
2575 v[12..20].copy_from_slice(&capacity.to_le_bytes());
2576 v[20..28].copy_from_slice(&GRAIN.to_le_bytes());
2577 v[44..48].copy_from_slice(&(NGTE as u32).to_le_bytes());
2578 v[56..64].copy_from_slice(&gd_sector.to_le_bytes()); let gd = gd_sector as usize * 512;
2581 v[gd..gd + 4].copy_from_slice(&(gt_sector as u32).to_le_bytes());
2582 v
2584 }
2585
2586 #[test]
2587 fn sesparse_descriptor_without_extent_errors() {
2588 use std::io::Write as _;
2589 let dir = tempfile::tempdir().unwrap();
2590 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"seSparse\"\n";
2591 let p = dir.path().join("disk.vmdk");
2592 std::fs::File::create(&p)
2593 .unwrap()
2594 .write_all(desc.as_bytes())
2595 .unwrap();
2596 assert!(matches!(
2597 VmdkFileReader::open_path(&p),
2598 Err(VmdkError::MalformedDescriptor(_))
2599 ));
2600 }
2601
2602 #[test]
2603 fn sparse_empty_grain_table_entry_reads_zero_and_iterates_empty() {
2604 let vmdk = sparse_with_zero_gd_entry();
2605 let mut r = VmdkReader::open(Cursor::new(vmdk)).expect("open");
2606 let lba = 512 * 8; assert!(!r.is_allocated(lba).expect("is_allocated"));
2609 r.seek(SeekFrom::Start(lba * 512)).unwrap();
2611 let mut buf = [0xFFu8; 512];
2612 r.read_exact(&mut buf).unwrap();
2613 assert_eq!(buf, [0u8; 512]);
2614 assert!(r.iter_allocated_grains().expect("iter").is_empty());
2616 }
2617
2618 #[test]
2619 fn flat_zero_capacity_iter_is_empty() {
2620 use std::io::Write as _;
2622 let dir = tempfile::tempdir().unwrap();
2623 let desc = "# Disk DescriptorFile\nversion=1\nCID=ffffffff\nparentCID=ffffffff\ncreateType=\"monolithicFlat\"\nRW 0 ZERO\n";
2624 let p = dir.path().join("empty.vmdk");
2625 std::fs::File::create(&p)
2626 .unwrap()
2627 .write_all(desc.as_bytes())
2628 .unwrap();
2629 let mut r = VmdkFileReader::open_path(&p).expect("open empty flat");
2630 assert_eq!(r.virtual_disk_size(), 0);
2631 assert!(r.iter_allocated_grains().expect("iter").is_empty());
2632 }
2633}