Skip to content

Commit 8ad6252

Browse files
authored
Improve VMDK extent descriptor parsing (#44)
1 parent 98a9a66 commit 8ad6252

2 files changed

Lines changed: 148 additions & 34 deletions

File tree

dissect/hypervisor/disk/vmdk.py

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from __future__ import annotations
2+
13
import ctypes
24
import io
35
import logging
46
import os
57
import textwrap
68
import zlib
79
from bisect import bisect_right
10+
from dataclasses import dataclass
811
from functools import lru_cache
912
from pathlib import Path
1013

@@ -59,13 +62,13 @@ def __init__(self, fh):
5962
if self.descriptor.attr["parentCID"] != "ffffffff":
6063
self.parent = open_parent(path.parent, self.descriptor.attr["parentFileNameHint"])
6164

62-
for _, size, extent_type, filename in self.descriptor.extents:
63-
if extent_type in ["SPARSE", "VMFSSPARSE", "SESPARSE"]:
64-
sdisk_fh = path.with_name(filename).open("rb")
65+
for extent in self.descriptor.extents:
66+
if extent.type in ["SPARSE", "VMFSSPARSE", "SESPARSE"]:
67+
sdisk_fh = path.with_name(extent.filename).open("rb")
6568
self.disks.append(SparseDisk(sdisk_fh, parent=self.parent))
66-
elif extent_type in ["VMFS", "FLAT"]:
67-
rdisk_fh = path.with_name(filename).open("rb")
68-
self.disks.append(RawDisk(rdisk_fh, size * SECTOR_SIZE))
69+
elif extent.type in ["VMFS", "FLAT"]:
70+
rdisk_fh = path.with_name(extent.filename).open("rb")
71+
self.disks.append(RawDisk(rdisk_fh, extent.sectors * SECTOR_SIZE))
6972

7073
elif magic in (COWD_MAGIC, VMDK_MAGIC, SESPARSE_MAGIC):
7174
sparse_disk = SparseDisk(fh)
@@ -398,18 +401,53 @@ def __getattr__(self, attr):
398401
return getattr(self.hdr, attr)
399402

400403

404+
@dataclass
405+
class ExtentDescriptor:
406+
access_mode: str
407+
sectors: int
408+
type: str
409+
filename: str | None = None
410+
start_sector: int | None = None
411+
partition_uuid: str | None = None
412+
device_identifier: str | None = None
413+
414+
def __post_init__(self) -> None:
415+
self._raw = " ".join(map(str, [v for v in self.__dict__.values() if v is not None]))
416+
self.sectors = int(self.sectors)
417+
418+
if self.filename:
419+
self.filename = self.filename.strip('"')
420+
421+
if self.start_sector:
422+
self.start_sector = int(self.start_sector)
423+
424+
def __repr__(self) -> str:
425+
return f"<ExtentDescriptor {self._raw}>"
426+
427+
def __str__(self) -> str:
428+
return self._raw
429+
430+
401431
class DiskDescriptor:
402-
def __init__(self, attr, extents, disk_db, sectors, raw_config=None):
432+
def __init__(
433+
self, attr: dict, extents: list[ExtentDescriptor], disk_db: dict, sectors: int, raw_config: str | None = None
434+
):
403435
self.attr = attr
404436
self.extents = extents
405437
self.ddb = disk_db
406438
self.sectors = sectors
407439
self.raw = raw_config
408440

409441
@classmethod
410-
def parse(cls, vmdk_config):
442+
def parse(cls, vmdk_config: str) -> DiskDescriptor:
443+
"""Return :class:`DiskDescriptor` based on the provided ``vmdk_config``.
444+
445+
Resources:
446+
- https://github.com/libyal/libvmdk/blob/main/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc
447+
""" # noqa: E501
448+
411449
descriptor_settings = {}
412-
extents = []
450+
extents: list[ExtentDescriptor] = []
413451
disk_db = {}
414452
sectors = 0
415453

@@ -420,11 +458,16 @@ def parse(cls, vmdk_config):
420458
continue
421459

422460
if line.startswith("RW ") or line.startswith("RDONLY ") or line.startswith("NOACCESS "):
423-
access_type, size, extent_type, filename = line.split(" ", 3)
424-
filename = filename.strip('"')
425-
size = int(size)
426-
sectors += size
427-
extents.append([access_type, size, extent_type, filename])
461+
# Extent descriptors can have up to seven values according to libvmdk documentation.
462+
parts = line.split(" ", maxsplit=6)
463+
464+
if len(parts) < 3:
465+
log.warning("Unexpected ExtentDescriptor format in vmdk config: %s, ignoring", line)
466+
continue
467+
468+
extent = ExtentDescriptor(*parts)
469+
sectors += extent.sectors
470+
extents.append(extent)
428471
continue
429472

430473
setting, _, value = line.partition("=")
@@ -438,35 +481,33 @@ def parse(cls, vmdk_config):
438481

439482
return cls(descriptor_settings, extents, disk_db, sectors, vmdk_config)
440483

441-
def __str__(self):
442-
str_template = """\
443-
# Disk DescriptorFile
444-
version=1
445-
{}
484+
def __str__(self) -> str:
485+
str_template = textwrap.dedent(
486+
"""\
487+
# Disk DescriptorFile
488+
version=1
489+
{}
490+
491+
# Extent Description
492+
{}
446493
447-
# Extent Description
448-
{}
494+
# The Disk Data Base
495+
#DDB
449496
450-
# The Disk Data Base
451-
#DDB
497+
{}"""
498+
)
452499

453-
{}"""
454-
str_template = textwrap.dedent(str_template)
455500
descriptor_settings = []
456501
for setting, value in self.attr.items():
457-
if setting == "version":
458-
continue
459-
descriptor_settings.append("{}={}".format(setting, value))
502+
if setting != "version":
503+
descriptor_settings.append(f"{setting}={value}")
460504
descriptor_settings = "\n".join(descriptor_settings)
461505

462-
extents = []
463-
for access_type, size, extent_type, filename in self.extents:
464-
extents.append('{} {} {} "{}"'.format(access_type, size, extent_type, filename))
465-
extents = "\n".join(extents)
506+
extents = "\n".join(map(str, self.extents))
466507

467508
disk_db = []
468509
for setting, value in self.ddb.items():
469-
disk_db.append('{} = "{}"'.format(setting, value))
510+
disk_db.append(f'{setting} = "{value}"')
470511
disk_db = "\n".join(disk_db)
471512

472513
return str_template.format(descriptor_settings, extents, disk_db)

tests/test_vmdk.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import pytest
2+
13
from dissect.hypervisor.disk.c_vmdk import c_vmdk
2-
from dissect.hypervisor.disk.vmdk import VMDK
4+
from dissect.hypervisor.disk.vmdk import VMDK, DiskDescriptor, ExtentDescriptor
35

46

57
def test_vmdk_sesparse(sesparse_vmdk):
@@ -18,3 +20,74 @@ def test_vmdk_sesparse(sesparse_vmdk):
1820
assert header.version == 0x200000001
1921

2022
assert vmdk.read(0x1000000) == b"a" * 0x1000000
23+
24+
25+
@pytest.mark.parametrize(
26+
"extent_description, expected_extents",
27+
[
28+
(
29+
'RW 123456789 SPARSE "disk.vmdk"',
30+
[
31+
ExtentDescriptor(
32+
access_mode="RW",
33+
sectors=123456789,
34+
type="SPARSE",
35+
filename="disk.vmdk",
36+
partition_uuid=None,
37+
device_identifier=None,
38+
),
39+
],
40+
),
41+
(
42+
'RW 123456789 FLAT "disk-flat.vmdk" 0',
43+
[
44+
ExtentDescriptor(
45+
access_mode="RW",
46+
sectors=123456789,
47+
type="FLAT",
48+
filename="disk-flat.vmdk",
49+
start_sector=0,
50+
partition_uuid=None,
51+
device_identifier=None,
52+
)
53+
],
54+
),
55+
(
56+
"RDONLY 0 ZERO",
57+
[
58+
ExtentDescriptor(
59+
access_mode="RDONLY",
60+
sectors=0,
61+
type="ZERO",
62+
),
63+
],
64+
),
65+
(
66+
'NOACCESS 123456789 SPARSE "disk-sparse.vmdk" 123 partition-uuid device-id',
67+
[
68+
ExtentDescriptor(
69+
access_mode="NOACCESS",
70+
sectors=123456789,
71+
type="SPARSE",
72+
filename="disk-sparse.vmdk",
73+
start_sector=123,
74+
partition_uuid="partition-uuid",
75+
device_identifier="device-id",
76+
),
77+
],
78+
),
79+
("RW 1234567890", []),
80+
('RDONLY "file.vmdk"', []),
81+
("NOACCESS", []),
82+
],
83+
ids=("sparse", "flat", "zero", "sparse-ids", "bad-1", "bad-2", "bad-3"),
84+
)
85+
def test_vmdk_extent_description(extent_description: str, expected_extents: list) -> None:
86+
"""test if we correctly parse VMDK sparse and flat extent descriptions.
87+
88+
Resources:
89+
- https://github.com/libyal/libvmdk/blob/main/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc#22-extent-descriptions
90+
""" # noqa: E501
91+
92+
descriptor = DiskDescriptor.parse(extent_description)
93+
assert descriptor.extents == expected_extents

0 commit comments

Comments
 (0)