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