Skip to main content

imago/vmdk/
mod.rs

1//! VMDK implementation.
2
3use crate::format::builder::{FormatDriverBuilder, FormatDriverBuilderBase};
4use crate::format::drivers::FormatDriverInstance;
5use crate::format::gate::ImplicitOpenGate;
6use crate::format::wrapped::WrappedFormat;
7use crate::format::{Format, PreallocateMode};
8use crate::io_buffers::IoBuffer;
9use crate::misc_helpers::{invalid_data, ResultErrorContext};
10use crate::storage::ext::StorageExt;
11use crate::{FormatAccess, ShallowMapping, Storage, StorageOpenOptions};
12use async_trait::async_trait;
13use std::fmt::{self, Display, Formatter};
14use std::marker::PhantomData;
15use std::ops::{Range, RangeInclusive};
16use std::path::{Path, PathBuf};
17use std::str::FromStr;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::Arc;
20use std::{cmp, io};
21
22/// As usual, VMDK sector size is 512 bytes as a fixed value
23const VMDK_SECTOR_SIZE: u64 = 512;
24/// VMDK SPARSE data signature
25const VMDK4_MAGIC: u32 = 0x564d444b; // 'KDMV'
26/// Supported version range
27const VMDK_VERSION_RANGE: RangeInclusive<u32> = 1..=3;
28
29/// Represents the data storage for a VMDK extent
30#[derive(Debug, Clone)]
31enum VmdkStorage<S: Storage + 'static> {
32    /// A FLAT extent with a RAW file starting from the exact offset
33    Flat {
34        /// Storage object containing linear (raw) data
35        file: S,
36        /// Byte offset in `file` where the data for this extent begins
37        offset: u64,
38    },
39    /// A zero-filled extent
40    Zero,
41}
42
43/// VMDK extent information after parsing, before opening
44#[derive(Debug)]
45enum VmdkParsedStorage {
46    /// A FLAT extent with a RAW file starting from the exact offset
47    Flat {
48        /// Path to storage object containing linear (raw) data
49        filename: String,
50        /// Offset, in 512-byte sectors (as written in the VMDK descriptor), where
51        /// the data for this extent begins in the storage object
52        offset: u64,
53    },
54    /// A zero-filled extent
55    Zero,
56}
57
58/// Access type for VMDK extents
59#[derive(Debug, Clone, PartialEq)]
60enum VmdkAccessType {
61    /// Read-write access
62    RW,
63    /// Read-only access
64    RdOnly,
65    /// No access
66    NoAccess,
67}
68
69/// VMDK extent
70#[derive(Debug)]
71struct VmdkExtent<S: Storage + 'static> {
72    /// Access type (RW, RDONLY, NOACCESS).
73    access_type: VmdkAccessType,
74    /// Part of the virtual disk covered by this extent.
75    ///
76    /// The start is equal to the end of the extent before it (0 if none), and the end is equal to
77    /// the start plus this extent’s length.
78    disk_range: Range<u64>,
79    /// Data source
80    ///
81    /// Present if and only if the access type is not NOACCESS.
82    storage: Option<VmdkStorage<S>>,
83}
84
85/// VMDK extent descriptor information after parsing, before opening
86#[derive(Debug)]
87struct VmdkParsedExtent {
88    /// Access type (RW, RDONLY, NOACCESS).
89    access_type: VmdkAccessType,
90    /// Number of sectors.
91    sectors: u64,
92    /// Data source
93    ///
94    /// Present if and only if the access type is not NOACCESS.
95    storage: Option<VmdkParsedStorage>,
96}
97
98/// VMDK disk image format implementation.
99#[derive(Debug)]
100pub struct Vmdk<S: Storage + 'static, F: WrappedFormat<S> + 'static = FormatAccess<S>> {
101    /// Storage object containing the VMDK descriptor file
102    descriptor_file: Arc<S>,
103
104    /// Backing image type.
105    ///
106    /// We do not support backing (parent) images yet, but capture the type so that when we do
107    /// support it, the change will be syntactically compatible.
108    parent_type: PhantomData<F>,
109
110    /// Base options to be used for implicitly opened storage objects.
111    storage_open_options: StorageOpenOptions,
112
113    /// Virtual disk size in bytes.
114    size: AtomicU64,
115
116    /// Parsed VMDK descriptor.
117    desc: VmdkDesc,
118
119    /// Extent information as parsed from the VMDK descriptor file.
120    parsed_extents: Vec<VmdkParsedExtent>,
121
122    /// Storage objects for each extent.
123    extents: Vec<VmdkExtent<S>>,
124}
125
126/// VMDK descriptor information.
127#[derive(Debug, Clone)]
128struct VmdkDesc {
129    /// Version number of the VMDK descriptor
130    version: u32,
131    /// Content ID
132    cid: String,
133    /// Content ID of the parent link
134    parent_cid: String,
135    /// Type of virtual disk
136    create_type: String,
137    /// The disk geometry value (sectors)
138    sectors: u64,
139    /// The disk geometry value (heads)
140    heads: u64,
141    /// The disk geometry value (cylinders)
142    cylinders: u64,
143}
144
145impl VmdkParsedExtent {
146    /// Parse an extent descriptor line.
147    fn try_from_descriptor_line(line: &str) -> io::Result<VmdkParsedExtent> {
148        // See https://github.com/libyal/libvmdk/blob/main/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc#221-extent-descriptor
149
150        let mut parts = line.split_whitespace();
151
152        let access_type = match parts
153            .next()
154            .ok_or_else(|| invalid_data("Access type missing"))?
155        {
156            "RW" => VmdkAccessType::RW,
157            "RDONLY" => VmdkAccessType::RdOnly,
158            "NOACCESS" => VmdkAccessType::NoAccess,
159            other => return Err(invalid_data(format!("Invalid access type '{other}'"))),
160        };
161
162        let sectors = parts
163            .next()
164            .ok_or_else(|| invalid_data("Sector count missing"))?
165            .parse()
166            .map_err(|_| invalid_data("Invalid sector count"))?;
167
168        if access_type == VmdkAccessType::NoAccess {
169            return Ok(VmdkParsedExtent {
170                access_type,
171                sectors,
172                storage: None,
173            });
174        }
175
176        let extent_type = parts
177            .next()
178            .ok_or_else(|| invalid_data("Extent type missing"))?;
179        if extent_type == "ZERO" {
180            return Ok(VmdkParsedExtent {
181                access_type,
182                sectors,
183                storage: Some(VmdkParsedStorage::Zero),
184            });
185        }
186        if extent_type != "FLAT" {
187            return Err(io::Error::new(
188                io::ErrorKind::Unsupported,
189                format!("Unsupported extent type {extent_type}"),
190            ));
191        }
192
193        // filename is enclosed in quotes and may contain spaces, so split the whole line by quotes
194        // (We could simplify this if we could do `line.splitn_whitespace(4)` at the beginning of
195        // this function, but `splitn_whitespace()` does not exist.)
196        let mut quote_split = line.splitn(3, '"').map(|part| part.trim());
197        // We know the line isn’t empty, so we must at least get one part
198        let before_filename = quote_split.next().unwrap();
199        let filename = quote_split
200            .next()
201            .ok_or_else(|| invalid_data("Extent filename missing"))?;
202        let after_filename = quote_split
203            .next()
204            .ok_or_else(|| invalid_data("Extent filename not terminated"))?;
205
206        let part_count_before_filename = before_filename.split_whitespace().count();
207        if part_count_before_filename != 3 {
208            return Err(invalid_data(format!(
209                "Expected filename at field index 3, found at {part_count_before_filename}"
210            )));
211        }
212
213        // Continue parsing after filename
214        parts = after_filename.split_whitespace();
215
216        let offset = parts
217            .next()
218            .map_or(Ok(0), |ofs_str| ofs_str.parse())
219            .map_err(|_| invalid_data("Invalid offset"))?;
220
221        Ok(VmdkParsedExtent {
222            access_type,
223            sectors,
224            storage: Some(VmdkParsedStorage::Flat {
225                filename: filename.to_string(),
226                offset,
227            }),
228        })
229    }
230}
231
232/// Remove double quotes around `input` if there are any.
233fn strip_quotes(input: &str) -> &str {
234    input
235        .strip_prefix('"')
236        .and_then(|value| value.strip_suffix('"'))
237        .unwrap_or(input)
238}
239
240/// Helper to parse an integer from the descriptor file.
241fn parse_desc_value<F: FromStr>(key: &str, value: &str) -> io::Result<F> {
242    let stripped = strip_quotes(value);
243
244    stripped
245        .parse::<F>()
246        .map_err(|_| invalid_data(format!("Invalid '{key}' value: {stripped}")))
247}
248
249impl<S: Storage + 'static, F: WrappedFormat<S> + 'static> Vmdk<S, F> {
250    /// Create a new [`FormatDriverBuilder`] instance for the given image.
251    pub fn builder(image: S) -> VmdkOpenBuilder<S, F> {
252        VmdkOpenBuilder::new(image)
253    }
254
255    /// Create a new [`FormatDriverBuilder`] instance for an image under the given path.
256    pub fn builder_path<P: AsRef<Path>>(image_path: P) -> VmdkOpenBuilder<S, F> {
257        VmdkOpenBuilder::new_path(image_path)
258    }
259
260    /// Open an extent from the information in `extent`.
261    ///
262    /// `in_disk_offset` is the offset in the virtual disk where this extent fits in.  It should be
263    /// the end offset of the extent before it.
264    async fn open_implicit_extent<G: ImplicitOpenGate<S>>(
265        &self,
266        extent: &VmdkParsedExtent,
267        in_disk_offset: u64,
268        open_gate: &mut G,
269    ) -> io::Result<VmdkExtent<S>> {
270        let sectors = extent.sectors;
271        let size = sectors.checked_mul(VMDK_SECTOR_SIZE).ok_or_else(|| {
272            invalid_data(format!(
273                "Extent size overflow: {sectors} * {VMDK_SECTOR_SIZE}"
274            ))
275        })?;
276        let disk_range = in_disk_offset..in_disk_offset.checked_add(size).ok_or_else(|| {
277            invalid_data(format!("Extent offset overflow: {in_disk_offset} + {size}"))
278        })?;
279
280        let Some(storage) = extent.storage.as_ref() else {
281            return Ok(VmdkExtent {
282                access_type: extent.access_type.clone(),
283                disk_range,
284                storage: None,
285            });
286        };
287
288        let storage = match storage {
289            VmdkParsedStorage::Flat { filename, offset } => {
290                let absolute = self
291                    .descriptor_file
292                    .resolve_relative_path(filename)
293                    .err_context(|| format!("Cannot resolve storage file name {filename}"))?;
294
295                let mut file_opts = self.storage_open_options.clone().filename(absolute.clone());
296                if extent.access_type == VmdkAccessType::RdOnly {
297                    file_opts = file_opts.write(false);
298                }
299
300                let file = open_gate
301                    .open_storage(file_opts)
302                    .await
303                    .err_context(|| format!("Data storage file {absolute:?}"))?;
304
305                VmdkStorage::Flat {
306                    file,
307                    // The FLAT offset is in 512-byte sectors (like the extent length);
308                    // scale it to bytes to match the byte-based `disk_range`.
309                    offset: (*offset).checked_mul(VMDK_SECTOR_SIZE).ok_or_else(|| {
310                        invalid_data(format!(
311                            "Extent offset overflow: {offset} * {VMDK_SECTOR_SIZE}"
312                        ))
313                    })?,
314                }
315            }
316
317            VmdkParsedStorage::Zero => VmdkStorage::Zero,
318        };
319
320        Ok(VmdkExtent {
321            access_type: extent.access_type.clone(),
322            disk_range,
323            storage: Some(storage),
324        })
325    }
326
327    /// Checks if the VMDK version is supported and returns an error if not
328    fn error_out_unsupported_version(&self) -> io::Result<()> {
329        let version = self.desc.version;
330        if !VMDK_VERSION_RANGE.contains(&version) {
331            return Err(io::Error::new(
332                io::ErrorKind::Unsupported,
333                format!("unsupported version {version}"),
334            ));
335        }
336        Ok(())
337    }
338
339    /// Parse a line in the VMDK descriptor file
340    fn parse_descriptor_line(&mut self, line: &str) -> io::Result<()> {
341        let line = line.trim();
342
343        if line.is_empty() || line.starts_with('#') {
344            return Ok(());
345        }
346
347        // Parse extent descriptors (RW/RDONLY/NOACCESS)
348        if let Some((access, _)) = line.split_once(char::is_whitespace) {
349            if matches!(access, "RW" | "RDONLY" | "NOACCESS") {
350                let extent = VmdkParsedExtent::try_from_descriptor_line(line)?;
351                self.parsed_extents.push(extent);
352                return Ok(());
353            }
354        }
355
356        let Some((key, value)) = line.split_once('=') else {
357            // Silently ignore
358            return Ok(());
359        };
360        let key = key.trim();
361        let value = value.trim();
362
363        match key {
364            "version" => {
365                self.desc.version = value
366                    .parse()
367                    .map_err(|_| invalid_data("Invalid version format"))?;
368            }
369            "CID" => self.desc.cid = value.to_string(),
370            "parentCID" => self.desc.parent_cid = value.to_string(),
371            "createType" => self.desc.create_type = strip_quotes(value).to_string(),
372            "parentFileNameHint" => {
373                return Err(io::Error::new(
374                    io::ErrorKind::Unsupported,
375                    "unsupported VMDK differential image (delta link)",
376                ))
377            }
378            "ddb.geometry.sectors" => self.desc.sectors = parse_desc_value(key, value)?,
379            "ddb.geometry.heads" => self.desc.heads = parse_desc_value(key, value)?,
380            "ddb.geometry.cylinders" => self.desc.cylinders = parse_desc_value(key, value)?,
381
382            // Ignore unidentified "ddb." (The Disk Database) items
383            key if key.starts_with("ddb.") => (),
384
385            key => {
386                return Err(invalid_data(format!(
387                    "Unrecognized VMDK descriptor file key '{key}'"
388                )))
389            }
390        }
391
392        Ok(())
393    }
394
395    /// Read and parse the VMDK descriptor by reading in lines until we find the end
396    async fn parse_descriptor_file(&mut self) -> io::Result<()> {
397        let desc_file_sz = self.descriptor_file.size()?;
398        if desc_file_sz < 4 {
399            return Err(invalid_data("VMDK descriptor file too short"));
400        }
401        // Sanity check to avoid unbounded allocation
402        if desc_file_sz > 2 * 1024 * 1024 {
403            return Err(invalid_data(
404                "VMDK descriptor file too long (max. 2 MB supported)",
405            ));
406        }
407
408        let desc_file_sz: usize = desc_file_sz.try_into().unwrap();
409        let mut desc_file = IoBuffer::new(desc_file_sz, self.descriptor_file.mem_align())?;
410        self.descriptor_file.read(desc_file.as_mut(), 0).await?;
411
412        let desc_file = desc_file.as_ref().into_slice();
413
414        // Check if it's a SPARSE format, bail it out now
415        if u32::from_le_bytes(desc_file[..4].try_into().unwrap()) == VMDK4_MAGIC {
416            return Err(io::Error::new(
417                io::ErrorKind::Unsupported,
418                "Unsupported VMDK sparse data file",
419            ));
420        }
421
422        for (line_i, line) in desc_file.split(|chr| *chr == b'\n').enumerate() {
423            let line = str::from_utf8(line).map_err(|e| {
424                invalid_data(format!(
425                    "{}: Line {}: {e}",
426                    self.descriptor_file,
427                    line_i + 1
428                ))
429            })?;
430
431            self.parse_descriptor_line(line)
432                .err_context(|| format!("{}: Line {}", self.descriptor_file, line_i + 1))?;
433        }
434
435        self.error_out_unsupported_version()?;
436        self.size = self
437            .parsed_extents
438            .iter()
439            .try_fold(0u64, |sum, extent| {
440                let sectors = extent.sectors;
441                let size = sectors.checked_mul(VMDK_SECTOR_SIZE).ok_or_else(|| {
442                    invalid_data(format!(
443                        "Extent size overflow: {sectors} * {VMDK_SECTOR_SIZE}"
444                    ))
445                })?;
446                sum.checked_add(size)
447                    .ok_or_else(|| invalid_data(format!("Extent offset overflow: {sum} + {size}")))
448            })?
449            .into();
450
451        Ok(())
452    }
453
454    /// Internal implementation for opening a VMDK image.
455    async fn do_open(
456        descriptor_file: S,
457        storage_open_options: StorageOpenOptions,
458    ) -> io::Result<Self> {
459        let mut vmdk = Vmdk {
460            descriptor_file: Arc::new(descriptor_file),
461            parent_type: PhantomData,
462            desc: VmdkDesc {
463                version: 0,
464                cid: String::new(),
465                parent_cid: String::new(),
466                create_type: String::new(),
467                sectors: 0,
468                heads: 0,
469                cylinders: 0,
470            },
471            parsed_extents: vec![],
472            extents: vec![],
473            size: 0.into(),
474            storage_open_options,
475        };
476
477        vmdk.parse_descriptor_file().await?;
478        Ok(vmdk)
479    }
480
481    /// Opens a VMDK file.
482    ///
483    /// This will not open any other storage objects needed, i.e. no extent data files.  Handling
484    /// those manually is not yet supported, so you have to make use of the implicit references
485    /// given in the image header, for which you can use
486    /// [`Vmdk::open_implicit_dependencies_gated()`].
487    pub async fn open_image(descriptor_file: S, writable: bool) -> io::Result<Self> {
488        if writable {
489            return Err(io::Error::new(
490                io::ErrorKind::Unsupported,
491                "No VMDK write support",
492            ));
493        }
494        Self::do_open(descriptor_file, StorageOpenOptions::new()).await
495    }
496
497    /// Open all implicit dependencies.
498    ///
499    /// In the case of VMDK, these are the extent data files.
500    pub async fn open_implicit_dependencies_gated<G: ImplicitOpenGate<S>>(
501        &mut self,
502        mut gate: G,
503    ) -> io::Result<()> {
504        if self.extents.is_empty() {
505            let mut in_disk_offset = 0;
506            for extent in &self.parsed_extents {
507                let opened = self
508                    .open_implicit_extent(extent, in_disk_offset, &mut gate)
509                    .await?;
510                in_disk_offset = opened.disk_range.end;
511                self.extents.push(opened);
512            }
513        }
514
515        Ok(())
516    }
517
518    /// Return the extent covering `offset`, if any.
519    fn get_extent_at(&self, offset: u64) -> Option<&VmdkExtent<S>> {
520        self.extents
521            .binary_search_by(|extent| {
522                if extent.disk_range.contains(&offset) {
523                    cmp::Ordering::Equal
524                } else if extent.disk_range.end <= offset {
525                    // disk_range is half-open [start, end); use <= so that
526                    // end == offset returns Less, not Greater.
527                    cmp::Ordering::Less
528                } else {
529                    cmp::Ordering::Greater
530                }
531            })
532            .ok()
533            .map(|index| &self.extents[index])
534    }
535}
536
537impl<S: Storage + 'static, F: WrappedFormat<S> + 'static> Display for Vmdk<S, F> {
538    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
539        write!(f, "vmdk[{}]", self.descriptor_file)
540    }
541}
542
543#[async_trait(?Send)]
544impl<S: Storage + 'static, F: WrappedFormat<S> + 'static> FormatDriverInstance for Vmdk<S, F> {
545    type Storage = S;
546
547    fn format(&self) -> Format {
548        Format::Vmdk
549    }
550
551    async unsafe fn probe(storage: &S) -> io::Result<bool>
552    where
553        Self: Sized,
554    {
555        // Check that the potential descriptor file has a reasonable length, is utf8, and contains
556        // a supported `version` key.
557        // (Or has the `VMDK4_MAGIC`.)
558
559        let desc_file_size = storage.size()?;
560        if !(4..=2 * 1024 * 1024).contains(&desc_file_size) {
561            return Ok(false);
562        }
563
564        let desc_file_size: usize = desc_file_size.try_into().unwrap();
565        let mut desc_file = IoBuffer::new(desc_file_size, storage.mem_align())?;
566        storage.read(desc_file.as_mut(), 0).await?;
567
568        let desc_file = desc_file.as_ref().into_slice();
569        if u32::from_le_bytes(desc_file[..4].try_into().unwrap()) == VMDK4_MAGIC {
570            return Ok(true);
571        }
572
573        for line in desc_file.split(|chr| *chr == b'\n') {
574            let Ok(line) = str::from_utf8(line) else {
575                return Ok(false);
576            };
577
578            let Some((key, value)) = line.split_once('=') else {
579                continue;
580            };
581            if key.trim() == "version" {
582                let Ok(version) = value.trim().parse() else {
583                    return Ok(false);
584                };
585                return Ok(VMDK_VERSION_RANGE.contains(&version));
586            }
587        }
588
589        Ok(false)
590    }
591
592    fn size(&self) -> u64 {
593        self.size.load(Ordering::Relaxed)
594    }
595
596    fn zero_granularity(&self) -> Option<u64> {
597        None
598    }
599
600    fn collect_storage_dependencies(&self) -> Vec<&S> {
601        let mut v = vec![self.descriptor_file.as_ref()];
602        for e in &self.extents {
603            let Some(storage) = e.storage.as_ref() else {
604                continue;
605            };
606            match storage {
607                VmdkStorage::Flat { file, offset: _ } => v.push(file),
608                VmdkStorage::Zero => (),
609            }
610        }
611        v
612    }
613
614    fn writable(&self) -> bool {
615        false
616    }
617
618    async fn get_mapping<'a>(
619        &'a self,
620        offset: u64,
621        max_length: u64,
622    ) -> io::Result<(ShallowMapping<'a, S>, u64)> {
623        let max_length = match self.size().checked_sub(offset) {
624            None | Some(0) => return Ok((ShallowMapping::Eof {}, 0)),
625            Some(remaining) => cmp::min(remaining, max_length),
626        };
627
628        let Some(extent) = self.get_extent_at(offset) else {
629            return Ok((ShallowMapping::Eof {}, 0));
630        };
631        // `get_extent_at` guarantees this won’t underflow
632        let in_extent_offset = offset - extent.disk_range.start;
633
634        let writable = match extent.access_type {
635            VmdkAccessType::RW => true,
636            VmdkAccessType::RdOnly => false,
637            VmdkAccessType::NoAccess => {
638                // Is that right?  Should this be ::Special?
639                return Err(io::Error::other("NOACCESS extent is accessed"));
640            }
641        };
642
643        // `access_type != NoAccess`, so `unwrap()` is safe
644        let mapping = match extent.storage.as_ref().unwrap() {
645            VmdkStorage::Flat {
646                file,
647                offset: base_offset,
648            } => ShallowMapping::Raw {
649                storage: file,
650                offset: base_offset.checked_add(in_extent_offset).ok_or_else(|| {
651                    invalid_data(format!(
652                        "Extent offset overflow: {base_offset} + {in_extent_offset}"
653                    ))
654                })?,
655                writable,
656            },
657
658            VmdkStorage::Zero => ShallowMapping::Zero { explicit: true },
659        };
660
661        Ok((
662            mapping,
663            cmp::min(max_length, extent.disk_range.end - offset),
664        ))
665    }
666
667    async fn ensure_data_mapping<'a>(
668        &'a self,
669        _offset: u64,
670        _length: u64,
671        _overwrite: bool,
672    ) -> io::Result<(&'a S, u64, u64)> {
673        Err(io::Error::other("Image is read-only"))
674    }
675
676    async fn flush(&self) -> io::Result<()> {
677        Ok(())
678    }
679
680    async fn sync(&self) -> io::Result<()> {
681        Ok(())
682    }
683
684    async unsafe fn invalidate_cache(&self) -> io::Result<()> {
685        Ok(())
686    }
687
688    async fn resize_grow(&self, _new_size: u64, _prealloc_mode: PreallocateMode) -> io::Result<()> {
689        Err(io::Error::other("Image is read-only"))
690    }
691
692    async fn resize_shrink(&mut self, _new_size: u64) -> io::Result<()> {
693        Err(io::Error::other("Image is read-only"))
694    }
695}
696
697/// Options builder for opening a VMDK image.
698pub struct VmdkOpenBuilder<S: Storage + 'static, F: WrappedFormat<S> + 'static = FormatAccess<S>>(
699    FormatDriverBuilderBase<S>,
700    PhantomData<F>,
701);
702
703impl<S: Storage + 'static, F: WrappedFormat<S> + 'static> FormatDriverBuilder<S>
704    for VmdkOpenBuilder<S, F>
705{
706    type Format = Vmdk<S, F>;
707    const FORMAT: Format = Format::Vmdk;
708
709    fn new(image: S) -> Self {
710        VmdkOpenBuilder(FormatDriverBuilderBase::new(image), PhantomData)
711    }
712
713    fn new_path<P: AsRef<Path>>(path: P) -> Self {
714        VmdkOpenBuilder(FormatDriverBuilderBase::new_path(path), PhantomData)
715    }
716
717    fn write(mut self, writable: bool) -> Self {
718        self.0.set_write(writable);
719        self
720    }
721
722    fn storage_open_options(mut self, options: StorageOpenOptions) -> Self {
723        self.0.set_storage_open_options(options);
724        self
725    }
726
727    async fn open<G: ImplicitOpenGate<S>>(self, mut gate: G) -> io::Result<Self::Format> {
728        if self.0.get_writable() {
729            return Err(io::Error::new(
730                io::ErrorKind::Unsupported,
731                "No VMDK write support",
732            ));
733        }
734
735        let file = self.0.open_image(&mut gate).await?;
736        let mut vmdk = Vmdk::open_image(file, false).await?;
737        vmdk.open_implicit_dependencies_gated(gate).await?;
738        Ok(vmdk)
739    }
740
741    fn get_image_path(&self) -> Option<PathBuf> {
742        self.0.get_image_path()
743    }
744
745    fn get_writable(&self) -> bool {
746        self.0.get_writable()
747    }
748
749    fn get_storage_open_options(&self) -> Option<&StorageOpenOptions> {
750        self.0.get_storage_opts()
751    }
752}
753
754//--------------------------------------------------------------------------------------------------
755// Tests
756//--------------------------------------------------------------------------------------------------
757
758#[cfg(test)]
759mod tests {
760    use super::Vmdk;
761    use crate::file::File;
762    use crate::format::access::{FormatAccess, FormatReadPlanStep};
763    use crate::{FormatDriverBuilder, PermissiveImplicitOpenGate};
764    use std::io;
765
766    /// A FLAT extent's offset is in 512-byte sectors, so a nonzero-offset extent (the
767    /// 2nd+ slice of a >2 GiB file) must resolve to byte `offset * 512`, not `offset`.
768    #[test]
769    fn flat_nonzero_offset_is_scaled_sectors_to_bytes() -> io::Result<()> {
770        let runtime = tokio::runtime::Builder::new_current_thread().build()?;
771        runtime.block_on(async {
772            // No `tempfile` dev-dependency; use a pid-unique scratch dir.
773            let dir = std::env::temp_dir().join(format!("imago_vmdk_off_{}", std::process::id()));
774            std::fs::create_dir_all(&dir)?;
775            let flat_path = dir.join("layer.flat");
776            let desc_path = dir.join("disk.vmdk");
777
778            // 4-sector (2048-byte) backing file is enough for two 2-sector extents.
779            std::fs::write(&flat_path, vec![0u8; 4 * 512])?;
780
781            // Two FLAT extents into one file; the 2nd at a nonzero sector offset (2).
782            let desc = "# Disk DescriptorFile\n\
783                version=1\n\
784                CID=fffffffe\n\
785                parentCID=ffffffff\n\
786                createType=\"twoGbMaxExtentFlat\"\n\
787                \n\
788                RW 2 FLAT \"layer.flat\" 0\n\
789                RW 2 FLAT \"layer.flat\" 2\n\
790                \n\
791                ddb.geometry.cylinders = \"1\"\n\
792                ddb.geometry.heads = \"16\"\n\
793                ddb.geometry.sectors = \"63\"\n";
794            std::fs::write(&desc_path, desc)?;
795
796            let vmdk = Vmdk::<File>::builder_path(&desc_path)
797                .open(PermissiveImplicitOpenGate::default())
798                .await?;
799            let image = FormatAccess::new(vmdk);
800
801            // Resolve a read at the start of the 2nd extent (virtual offset 1024).
802            let plan = image.plan_read(1024, 512).await?;
803            let steps = plan.steps();
804            assert!(!steps.is_empty(), "expected a read step, got none");
805            // Assert on `offset` (resolved backing offset), not `image_offset`
806            // (the virtual offset, which is 1024 regardless of the bug).
807            let storage_offset = match &steps[0] {
808                FormatReadPlanStep::Raw { offset, .. } => *offset,
809                step => panic!("expected a Raw step, got {step:?}"),
810            };
811
812            // 2 sectors * 512 = 1024 (the bug yielded the raw sector value, 2).
813            assert_eq!(
814                storage_offset, 1024,
815                "FLAT offset must be scaled sectors->bytes; got {storage_offset}"
816            );
817
818            std::fs::remove_dir_all(&dir).ok();
819            Ok(())
820        })
821    }
822}