From 4fd5768427b3e31e81c1090c7999c01ceba99d79 Mon Sep 17 00:00:00 2001
From: Schamper <1254028+Schamper@users.noreply.github.com>
Date: Sun, 9 Apr 2023 22:23:48 +0200
Subject: [PATCH 1/6] Add support for Parallels HDD/HDS
---
dissect/hypervisor/__init__.py | 3 +-
dissect/hypervisor/disk/c_hdd.py | 82 ++++
dissect/hypervisor/disk/hdd.py | 404 ++++++++++++++++++
tests/conftest.py | 15 +
tests/data/expanding.hdd/DiskDescriptor.xml | 52 +++
tests/data/expanding.hdd/expanding.hdd | 0
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 115232 bytes
tests/data/plain.hdd/DiskDescriptor.xml | 52 +++
tests/data/plain.hdd/plain.hdd | 0
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 114982 bytes
tests/data/split.hdd/DiskDescriptor.xml | 102 +++++
tests/data/split.hdd/split.hdd | 0
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 3530 bytes
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 3531 bytes
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 3534 bytes
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 3534 bytes
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 3534 bytes
...baabe3-6958-40ff-92a7-860e329aab41}.hds.gz | Bin 0 -> 3534 bytes
tests/test_hdd.py | 83 ++++
19 files changed, 792 insertions(+), 1 deletion(-)
create mode 100644 dissect/hypervisor/disk/c_hdd.py
create mode 100644 dissect/hypervisor/disk/hdd.py
create mode 100644 tests/data/expanding.hdd/DiskDescriptor.xml
create mode 100644 tests/data/expanding.hdd/expanding.hdd
create mode 100644 tests/data/expanding.hdd/expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/data/plain.hdd/DiskDescriptor.xml
create mode 100644 tests/data/plain.hdd/plain.hdd
create mode 100644 tests/data/plain.hdd/plain.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/data/split.hdd/DiskDescriptor.xml
create mode 100644 tests/data/split.hdd/split.hdd
create mode 100644 tests/data/split.hdd/split.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/data/split.hdd/split.hdd.1.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/data/split.hdd/split.hdd.2.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/data/split.hdd/split.hdd.3.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/data/split.hdd/split.hdd.4.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/data/split.hdd/split.hdd.5.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
create mode 100644 tests/test_hdd.py
diff --git a/dissect/hypervisor/__init__.py b/dissect/hypervisor/__init__.py
index c872697..1ad7286 100644
--- a/dissect/hypervisor/__init__.py
+++ b/dissect/hypervisor/__init__.py
@@ -1,10 +1,11 @@
from dissect.hypervisor.backup import vma, wim, xva
from dissect.hypervisor.descriptor import hyperv, ovf, vbox, vmx
-from dissect.hypervisor.disk import qcow2, vdi, vhd, vhdx, vmdk
+from dissect.hypervisor.disk import hdd, qcow2, vdi, vhd, vhdx, vmdk
from dissect.hypervisor.util import envelope, vmtar
__all__ = [
"envelope",
+ "hdd",
"hyperv",
"ovf",
"qcow2",
diff --git a/dissect/hypervisor/disk/c_hdd.py b/dissect/hypervisor/disk/c_hdd.py
new file mode 100644
index 0000000..5c04158
--- /dev/null
+++ b/dissect/hypervisor/disk/c_hdd.py
@@ -0,0 +1,82 @@
+from dissect import cstruct
+
+hdd_def = """
+/* Compressed disk (version 1) */
+#define PRL_IMAGE_COMPRESSED 2
+
+/* Compressed disk v1 signature */
+#define SIGNATURE_STRUCTURED_DISK_V1 b"WithoutFreeSpace"
+
+/* Compressed disk v2 signature */
+#define SIGNATURE_STRUCTURED_DISK_V2 b"WithouFreSpacExt"
+
+/* Sign that the disk is in "using" state */
+#define SIGNATURE_DISK_IN_USE 0x746F6E59
+
+/**
+ * Compressed disk image flags
+ */
+#define CIF_NoFlags 0x00000000 /* No any flags */
+#define CIF_Empty 0x00000001 /* No any data was written */
+#define CIF_FmtVersionConvert 0x00000002 /* Version Convert in progree */
+#define CIF_FlagsMask (CIF_Empty | CIF_FmtVersionConvert)
+#define CIF_Invalid 0xFFFFFFFF /* Invalid flag */
+
+#define SECTOR_LOG 9
+#define DEF_CLUSTER_LOG 11 /* 1M cluster-block */
+#define DEF_CLUSTER (1 << (DEF_CLUSTER_LOG + SECTOR_LOG))
+
+/* Helpers to generate PVD-header based on requested bdsize */
+
+#define DEFAULT_HEADS_COUNT 16
+#define DEFAULT_SECTORS_COUNT 63
+#define SECTOR_SIZE (1 << SECTOR_LOG)
+
+struct pvd_header {
+ char m_Sig[16]; /* Signature */
+ uint32 m_Type; /* Disk type */
+ uint32 m_Heads; /* heads count */
+ uint32 m_Cylinders; /* tracks count */
+ uint32 m_Sectors; /* Sectors per track count */
+ uint32 m_Size; /* Size of disk in tracks */
+ union { /* Size of disk in 512-byte sectors */
+ struct {
+ uint32 m_SizeInSectors_v1;
+ uint32 Unused;
+ };
+ uint64 m_SizeInSectors_v2;
+ };
+ uint32 m_DiskInUse; /* Disk in use */
+ uint32 m_FirstBlockOffset; /* First data block offset (in sectors) */
+ uint32 m_Flags; /* Misc flags */
+ uint64 m_FormatExtensionOffset; /* Optional header offset in bytes */
+};
+
+struct pvd_ext_block_check {
+ // Format Extension magic = 0xAB234CEF23DCEA87
+ uint64 m_Magic;
+ // Md5 checksum of the whole (without top 24 bytes of block check)
+ // Format Extension Block.
+ uint8 m_Md5[16];
+};
+
+struct pvd_ext_block_element_header {
+ uint64 magic;
+ uint64 flags;
+ uint32 size;
+ uint32 unused32;
+};
+
+struct pvd_dirty_bitmap_raw {
+ uint64 m_Size;
+ uint8 m_Id[16];
+ uint32 m_Granularity;
+ uint32 m_L1Size;
+ uint64 m_L1[m_L1Size];
+};
+"""
+
+c_hdd = cstruct.cstruct()
+c_hdd.load(hdd_def)
+
+SECTOR_SIZE = c_hdd.SECTOR_SIZE
diff --git a/dissect/hypervisor/disk/hdd.py b/dissect/hypervisor/disk/hdd.py
new file mode 100644
index 0000000..a231c5f
--- /dev/null
+++ b/dissect/hypervisor/disk/hdd.py
@@ -0,0 +1,404 @@
+from __future__ import annotations
+
+from bisect import bisect_right
+from dataclasses import dataclass
+from functools import cached_property
+from pathlib import Path
+from typing import BinaryIO, Iterator, Optional, Tuple, Union
+from uuid import UUID
+from xml.etree.ElementTree import Element
+
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ from xml.etree import ElementTree
+
+from dissect.util.stream import AlignedStream
+
+from dissect.hypervisor.disk.c_hdd import SECTOR_SIZE, c_hdd
+from dissect.hypervisor.exceptions import InvalidHeaderError
+
+DEFAULT_TOP_GUID = UUID("{5fbaabe3-6958-40ff-92a7-860e329aab41}")
+
+
+class HDD:
+ """Parallels HDD virtual disk implementation.
+
+ Args:
+ path: The path to the .hdd directory or .hdd file in a .hdd directory.
+ """
+
+ def __init__(self, path: Path):
+ if path.is_file() and path.parent.suffix.lower() == ".hdd":
+ path = path.parent
+ self.path = path
+
+ descriptor_path = path.joinpath("DiskDescriptor.xml")
+ if not descriptor_path.exists():
+ raise ValueError(f"Invalid Parallels HDD path: {path} (missing DiskDescriptor.xml)")
+
+ self.descriptor = Descriptor(descriptor_path)
+
+ def _open_image(self, path: Path) -> BinaryIO:
+ """Helper method for opening image files relative to this HDD.
+
+ Args:
+ path: The path to the image file to open.
+ """
+ root = self.path
+ filename = path.name
+
+ if path.is_absolute():
+ # If the path is absolute, check if it exists
+ if not path.exists():
+ # If the absolute path does not exist, we're probably dealing with a HDD
+ # that's been copied or moved (e.g., uploaded or copied as evidence)
+ # Try a couple of common patterns to see if we can locate the file
+
+ # File is in same HDD directory
+ # root/example.pvm/example.hdd/absolute.ext
+ candidate_path = root / filename
+ if not candidate_path.exists():
+ # File is in a separate HDD directory in parent (VM) directory
+ # root/example.pvm/absolute.hdd/absolute.ext
+ candidate_path = root.parent / path.parent.name / filename
+
+ if not candidate_path.exists():
+ # File is in .pvm directory in parent of parent directory (linked clones)
+ # root/absolute.pvm/absolute.hdd/absolute.ext
+ candidate_path = root.parent.parent / path.parent.parent.name / path.parent.name / filename
+
+ path = candidate_path
+
+ return path.open("rb")
+
+ # If the path is relative, it's always relative to the HDD root
+ return (root / path).open("rb")
+
+ def open(self, guid: Optional[Union[str, UUID]] = None) -> BinaryIO:
+ """Open a stream for this HDD, optionally for a specific snapshot.
+
+ If no snapshot GUID is provided, the "top" snapshot will be used.
+
+ Args:
+ guid: The snapshot GUID to open.
+ """
+ if guid and not isinstance(guid, UUID):
+ guid = UUID(guid)
+
+ if guid is None:
+ guid = self.descriptor.snapshots.top_guid or DEFAULT_TOP_GUID
+
+ chain = self.descriptor.get_chain(guid)
+
+ streams = []
+ for storage in self.descriptor.storage_data.storages:
+ stream = None
+ for guid in chain[::-1]:
+ image = storage.find_image(guid)
+ fh = self._open_image(Path(image.file))
+
+ if image.type == "Compressed":
+ fh = HDS(fh, parent=stream)
+ elif image.type != "Plain":
+ raise ValueError(f"Unsupported image type: {image.type}")
+
+ stream = fh
+
+ streams.append((storage, stream))
+
+ return StorageStream(streams)
+
+
+class Descriptor:
+ """Helper class for working with ``DiskDescriptor.xml``.
+
+ Args:
+ path: The path to ``DiskDescriptor.xml``.
+ """
+
+ def __init__(self, path: Path):
+ self.path = path
+
+ self.xml: Element = ElementTree.fromstring(path.read_text())
+ self.storage_data = StorageData.from_xml(self.xml.find("StorageData"))
+ self.snapshots = Snapshots.from_xml(self.xml.find("Snapshots"))
+
+ def get_chain(self, guid: UUID) -> list[UUID]:
+ """Return the snapshot chain for a given snapshot GUID.
+
+ Args:
+ guid: The snapshot GUID to return a chain for.
+ """
+ shot = self.snapshots.find_shot(guid)
+
+ chain = [shot.guid]
+ while shot.parent != UUID("00000000-0000-0000-0000-000000000000"):
+ shot = self.snapshots.find_shot(shot.parent)
+ chain.append(shot.guid)
+
+ return chain
+
+
+@dataclass
+class StorageData:
+ storages: list[Storage]
+
+ @classmethod
+ def from_xml(cls, element: Element) -> StorageData:
+ if element.tag != "StorageData":
+ raise ValueError("Invalid StorageData XML element")
+
+ return cls(list(map(Storage.from_xml, element.iterfind("Storage"))))
+
+
+@dataclass
+class Storage:
+ start: int
+ end: int
+ images: list[Image]
+
+ @classmethod
+ def from_xml(cls, element: Element) -> Storage:
+ if element.tag != "Storage":
+ raise ValueError("Invalid Storage XML element")
+
+ start = int(element.find("Start").text)
+ end = int(element.find("End").text)
+ images = list(map(Image.from_xml, element.iterfind("Image")))
+
+ return cls(start, end, images)
+
+ def find_image(self, guid: UUID) -> Image:
+ """Find a specific image GUID.
+
+ Args:
+ guid: The image GUID to find.
+
+ Raises:
+ KeyError: If the GUID could not be found.
+ """
+ for image in self.images:
+ if image.guid == guid:
+ return image
+
+ raise KeyError(f"Image GUID not found: {guid}")
+
+
+@dataclass
+class Image:
+ guid: UUID
+ type: str
+ file: str
+
+ @classmethod
+ def from_xml(cls, element: Element) -> Image:
+ if element.tag != "Image":
+ raise ValueError("Invalid Image XML element")
+
+ return cls(
+ UUID(element.find("GUID").text),
+ element.find("Type").text,
+ element.find("File").text,
+ )
+
+
+@dataclass
+class Snapshots:
+ top_guid: Optional[UUID]
+ shots: list[Shot]
+
+ @classmethod
+ def from_xml(cls, element: Element) -> Snapshots:
+ if element.tag != "Snapshots":
+ raise ValueError("Invalid Snapshots XML element")
+
+ top_guid = element.find("TopGUID")
+ if top_guid:
+ top_guid = UUID(top_guid.text)
+ shots = list(map(Shot.from_xml, element.iterfind("Shot")))
+
+ return cls(top_guid, shots)
+
+ def find_shot(self, guid: UUID) -> Shot:
+ """Find a specific snapshot GUID.
+
+ Args:
+ guid: The snapshot GUID to find.
+
+ Raises:
+ KeyError: If the GUID could not be found.
+ """
+ for shot in self.shots:
+ if shot.guid == guid:
+ return shot
+
+ raise KeyError(f"Shot GUID not found: {guid}")
+
+
+@dataclass
+class Shot:
+ guid: UUID
+ parent: UUID
+
+ @classmethod
+ def from_xml(cls, element: Element) -> Shot:
+ if element.tag != "Shot":
+ raise ValueError("Invalid Shot XML element")
+
+ return cls(
+ UUID(element.find("GUID").text),
+ UUID(element.find("ParentGUID").text),
+ )
+
+
+class StorageStream(AlignedStream):
+ """Stream implementation for HDD streams.
+
+ HDD files can exist of one or multiple streams, starting at consecutive offsets.
+ This class stitches all streams together into a single stream.
+
+ Args:
+ streams: A list of :class:`Storage` and file-like object tuples.
+ """
+
+ def __init__(self, streams: list[tuple[Storage, BinaryIO]]):
+ self.streams = sorted(streams, key=lambda entry: entry[0].start)
+ self._lookup = []
+
+ size = 0
+ for storage, _ in self.streams:
+ self._lookup.append(storage.start)
+ size = storage.end
+
+ super().__init__(size * SECTOR_SIZE)
+
+ def _read(self, offset: int, length: int) -> bytes:
+ sector = offset // SECTOR_SIZE
+ count = (length + SECTOR_SIZE - 1) // SECTOR_SIZE
+
+ result = []
+ stream_idx = bisect_right(self._lookup, sector) - 1
+
+ while count > 0 and stream_idx < len(self.streams):
+ storage, stream = self.streams[stream_idx]
+ sectors_remaining = storage.end - sector
+ read_sectors = min(sectors_remaining, count)
+
+ stream.seek((sector - storage.start) * SECTOR_SIZE)
+ result.append(stream.read(read_sectors * SECTOR_SIZE))
+
+ sector += read_sectors
+ count -= read_sectors
+ stream_idx += 1
+
+ return b"".join(result)
+
+
+class HDS(AlignedStream):
+ """Parallels HDS implementation.
+
+ HDS is the format for Parallels sparse disk files.
+
+ Args:
+ fh: The file-like object to the HDS file.
+ parent: Optional file-like object for the parent HDS file.
+ """
+
+ def __init__(self, fh: BinaryIO, parent: Optional[BinaryIO] = None):
+ self.fh = fh
+ self.parent = parent
+
+ self.header = c_hdd.pvd_header(fh)
+ if self.header.m_Sig not in (c_hdd.SIGNATURE_STRUCTURED_DISK_V1, c_hdd.SIGNATURE_STRUCTURED_DISK_V2):
+ raise InvalidHeaderError(f"Invalid HDS header signature: {self.header.m_Sig}")
+
+ if self.header.m_Sig == c_hdd.SIGNATURE_STRUCTURED_DISK_V1:
+ size = self.header.m_SizeInSectors_v1
+ self._bat_step = self.header.m_Sectors
+ self._bat_multiplier = 1
+ else:
+ size = self.header.m_SizeInSectors_v2
+ self._bat_step = 1
+ self._bat_multiplier = self.header.m_Sectors
+
+ self.cluster_size = self.header.m_Sectors * SECTOR_SIZE
+
+ self.data_offset = self.header.m_FirstBlockOffset
+ self.in_use = self.header.m_DiskInUse == c_hdd.SIGNATURE_DISK_IN_USE
+
+ super().__init__(size * SECTOR_SIZE)
+
+ @cached_property
+ def bat(self) -> list[int]:
+ """Return the block allocation table (BAT)."""
+ self.fh.seek(len(c_hdd.pvd_header))
+ return c_hdd.uint32[self.header.m_Size](self.fh)
+
+ def _read(self, offset: int, length: int) -> bytes:
+ result = []
+
+ for read_offset, read_size in self._iter_runs(offset, length):
+ # Sentinel value for sparse runs
+ if read_offset is None:
+ if self.parent:
+ self.parent.seek(offset)
+ result.append(self.parent.read(read_size))
+ else:
+ result.append(b"\x00" * read_size)
+ else:
+ self.fh.seek(read_offset)
+ result.append(self.fh.read(read_size))
+
+ offset += read_size
+ length -= read_size
+
+ return b"".join(result)
+
+ def _iter_runs(self, offset: int, length: int) -> Iterator[Tuple[int, int]]:
+ """Iterate optimized read runs for a given offset and read length.
+
+ Args:
+ offset: The offset in bytes to generate runs for.
+ length: The length in bytes to generate runs for.
+ """
+ bat = self.bat
+
+ run_offset = None
+ run_size = 0
+
+ while offset < self.size and length > 0:
+ cluster_idx, offset_in_cluster = divmod(offset, self.cluster_size)
+ read_size = min(self.cluster_size - offset_in_cluster, length)
+
+ bat_entry = bat[cluster_idx]
+ if bat_entry == 0:
+ # BAT entry of 0 means either a sparse or a parent read
+ # Use 0 to denote a sparse run for now to make calculations easier
+ read_offset = 0
+ else:
+ read_offset = (bat_entry * self._bat_multiplier * SECTOR_SIZE) + offset_in_cluster
+
+ if run_offset is None:
+ # First iteration
+ run_offset = read_offset
+ run_size = read_size
+ elif read_offset == run_offset + run_size or (run_offset, read_offset) == (0, 0):
+ # Consecutive (sparse) clusters
+ run_size += read_size
+ else:
+ # New run
+ # Replace 0 with None as sparse sentinel value
+ yield (run_offset or None, run_size)
+
+ # Reset run
+ run_offset = read_offset
+ run_size = read_size
+
+ offset += read_size
+ length -= read_size
+
+ if run_offset is not None:
+ # Flush remaining run
+ # Replace 0 with None as sparse sentinel value
+ yield (run_offset or None, run_size)
diff --git a/tests/conftest.py b/tests/conftest.py
index 982f427..fedce99 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -63,6 +63,21 @@ def sesparse_vmdk():
yield from open_file_gz("data/sesparse.vmdk.gz")
+@pytest.fixture
+def plain_hdd():
+ yield absolute_path("data/plain.hdd")
+
+
+@pytest.fixture
+def expanding_hdd():
+ yield absolute_path("data/expanding.hdd")
+
+
+@pytest.fixture
+def split_hdd():
+ yield absolute_path("data/split.hdd")
+
+
@pytest.fixture
def simple_vma():
yield from open_file_gz("data/test.vma.gz")
diff --git a/tests/data/expanding.hdd/DiskDescriptor.xml b/tests/data/expanding.hdd/DiskDescriptor.xml
new file mode 100644
index 0000000..cefe2ca
--- /dev/null
+++ b/tests/data/expanding.hdd/DiskDescriptor.xml
@@ -0,0 +1,52 @@
+
+
+
+ 204800
+ 400
+ 4096
+ 512
+ 16
+ 32
+ 0
+
+ {00000000-0000-0000-0000-000000000000}
+
+
+
+ {0610bb35-447e-4aae-aa79-f1571d969081}
+ expanding
+
+ level2
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 204800
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Compressed
+ expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ {00000000-0000-0000-0000-000000000000}
+
+
+
diff --git a/tests/data/expanding.hdd/expanding.hdd b/tests/data/expanding.hdd/expanding.hdd
new file mode 100644
index 0000000..e69de29
diff --git a/tests/data/expanding.hdd/expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz b/tests/data/expanding.hdd/expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
new file mode 100644
index 0000000000000000000000000000000000000000..90789fd501dd710141614475befa43e6207bb153
GIT binary patch
literal 115232
zcmeI(f2`GY9mnyDG;)o)%{91X;KgCK#ynYRt`)P=W-K~lO2UY#k!E2hTgQH^0kzWT
z*GV)$3x_Qu3_V7I1*O=XV_ACE2Dtuc<(z8;c4oz!oa5fkSA5^S_Wry%|1r)VPyc!U
zxH!%p-#0I~^7V{wpE|T)gX7!#UXFc+T-#`1W-|jv#fA#KVt0unm
z!t;9{du+@84egoTo3Gw@;JM{vD@LB&J^ht0K6AmgC;z&7q9F8;%f+jrf2aM$Z+Zri=9-MrzSckeuWW&QB7+To>*9jDLizW?m=?prcz
z-_^7B-}uC*7oFW)dfkp~SA1-L^Dl>vjz69J8}HmY>xcUuytaAiEzQgKp4Yy3)4uCB
z?cct*dBs!Bk37`;=#d@wFMQ9wowE;az3bpT%Madn(=*pM&%f=?11l%axMk|lN&RP{
zyJ6_iqjRS8KE#^M6YYf$pVUv&`^neyar!(0bo$_MJN`h9IsZ)a@u`SDHH&7L~a@57(?ukAT&4d3GQVII`;;oIZnq4BU1l*!ek&e-UG
z#_u3{CSkmp1mYK$lSoBJFp3tPf$4=k$J3
z-e>M7KY$m+-zpvNf$^}8CsXY!otM++6zh;anH3k5srU{Zm&htQnTqcueO_U0(#Ku2
zazUyQdvv0R&giH{>?M7sS&Q_EKGn?xK|oQjOT76Ge1JM>S$E>2todNT29a&0J8X
z;yZL)BCF_RD!!BSxxgBvkGp8)f>a~+=tL2n(NT@qOZvRa>ZDKfsb(%HQ}G=-E|FDq
zG8NxR`n=m}q>sC3<$_cr_UJ?rozYQ^*h~7%w_(yJ`cyL)l&Sa*9hb-|I+=>^Bz-Ql
z?nAuq<1kXWAi*eFbP5t#pd%PXE9vt->ySPLP;bEnWh%Zy$0f3gPNw2JNuP_XP5QWt
zRxU_2VvkM~(HR}ph`prG2dqWZchSlPsYdM4i6T0qqZ+Z7
z^!cz2lRnX>nz^7%#dqkqL{`zsRD37tbE$QA^1hG5NacbAqiE47NMwPIU=*#S&t=vj
zeF~u7f(y!2e20!pWEGuE#dnfEms^|kaTl#zkZQypohYI+I;s(SNuR~mB7LGyHFH6k
zito^IiL9cNsrXLP=SpjkKJKEG3sQ~PqZ37RMn^SbFX^+?>ZDKfsb(%HQ}G=-E|FDq
zG8NxR`Yf{=>EkY1xggbuJvvcDXLM8}_L4r!ZJ6|lKGn^%h)Ers6wvTq3LJWGcRs^jTqT(#Ku2
zazUyQdvv0R&giH{>?M7!wHE0UeX5xY%2a%Zj!R?}olM1dl0Kib2I=E2TDc(Ah&?({
zL}zqVBleO$pRzjX6Md?g3(8b{hmK2R6`f4QcalCUtw#E|i&id3HDZrW6ww(S)rh^M
z&u46y^oc&z%mrmCzC*_)vWiZo;yX#7&sz6E-uH1Bsa%j?6fHUhi7e0&jG~qF`J8n~
zp8}}2;DRz0-=X6YSw$yP@tvg4=dDfpxQkXUNHt=QP887@9o2}vq|Z&(B7LGyHFH6k
zito^IiL9cNsrXLP=Zn@LecVMW7o-}oM<8l?{_V(WjcZpiISg=(t2y(aBVNC+V}s
zy1(RoABU031qnvcqEnE_0v*99T1lU;S%>r~fO-oqC{ytrIxdk_bTSp+N&0-<+N6)W
zXyt-bBlhS-5uMRdjo3^2+-xnE^S6eO}hM=*+3(r2A@
zNS^|zx8Q;@72l!b5?MtjQ}Lao&+XPGecVMW7o-}oM<?M8fv^wb%eX5xY%2a%Zj!R?}
zolM1dl0HAO8tLOMTDc(Ah&?({L}zqVBleO$ciAxM6Md?g3(8b{hmK2R6`f4QcalDv
zt@{h!_i-4hT##TCEjk5>EYJ~*qLuXdv2{qF0;sp(f-)7~q2m%+MJH47outoCtWEm3
zi&id3HDZrW6ww(S)rh^M&rhvI`b3{<=7KU6-=X6YSw$yP@tvg4ÎxQkXUNHt=Q
zP887@9o2}vq|d!pCw-z%HFH6kito^IiL9cNsrXLPXPebXA9vBp1*t~t(TO5DqoW$J
zm-M;chDo33Q_WmZrs6wvTq3LJWGcRs^m)L#5AeQ^!${?V1fyutDM(~7a5~RDI%i7n
b^R3xD(O&qle9)89&Y0Nx-^D{i7f$;>z|u#`
literal 0
HcmV?d00001
diff --git a/tests/data/plain.hdd/DiskDescriptor.xml b/tests/data/plain.hdd/DiskDescriptor.xml
new file mode 100644
index 0000000..c9653f3
--- /dev/null
+++ b/tests/data/plain.hdd/DiskDescriptor.xml
@@ -0,0 +1,52 @@
+
+
+
+ 204800
+ 400
+ 4096
+ 512
+ 16
+ 32
+ 0
+
+ {00000000-0000-0000-0000-000000000000}
+
+
+
+ {4be4afe0-ff6f-4544-b16c-d98d170a029c}
+ plain
+
+ level2
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 204800
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Plain
+ plain.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ {00000000-0000-0000-0000-000000000000}
+
+
+
diff --git a/tests/data/plain.hdd/plain.hdd b/tests/data/plain.hdd/plain.hdd
new file mode 100644
index 0000000..e69de29
diff --git a/tests/data/plain.hdd/plain.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz b/tests/data/plain.hdd/plain.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
new file mode 100644
index 0000000000000000000000000000000000000000..6a366d9b12e1fbebb252972b47b065ef6aa3dd38
GIT binary patch
literal 114982
zcmeI(U#R4D8OQNi4Ut%zh_FS%#;Ax0M@=LoM4}=hn}~>r2#qC0!V!s(irAt=?a%br
zGAa%c5fP33)Bb?D&P6xDc0HBeeKfGQ?Gg3>Cwf{p1tFAs{R_R=Y@CeLplCVD!emw-5*&SsE5!t)n}
z#dG@qYN)5Yn!K?6KGD-XwxK>RlCXPJBw;~ftD&CqYVzXt+eD8&siStN&x`2k9u?73
zkl1Rdr@Wf{d;1Nd$DY(tJJjbz^mLDk=qX5SHPlmHP5!fefatL&b<_^^c@aI`qau0=
z5?c-Rlvk7gZof+O*poVHhx)vTp6*c*Jq3xahI-1Y$>H`(M2|hGqjspzi|FYd712|W
z*lK{MeD1$djAHZGzn^--KI?^OuipKkywAK(et;LmKWHEKz<5~4$+UgbIhQ^c#hUb~
zthk^`MLTpPQB`y*740m2UKUHzM=oBuAlJw}I$6YLbX+6%mOhung7nEgZRUb1746WG
zL{-tLRJ617xisdak6gTRL9UT|bh3!g=(tAiEqz`dGtwvfw3!R4RJ21!5>-W~Qqj)R
z=VVMtAGvttf?Ol_=wuO}(Q%F3Tl%~r#-va7X)_m8sc46eB&v!|rJ|js&nsi|7vA>~
zj8!hkFuE3Hz|7)IAx`n)REq)!L5TW~>@igxHoqN?arD%x54ygHVok6gTRL9UT|
zbh3!g=(tAiEqyMJ1?iK0+RO!2D%znViK?Pgsc2{Eb1LSfk6gTRL9UT|bh3!g=(tAi
zEq$(t8R?UK+RO!2D%znViK?Pgsc2{E^V*n_K63HO1-VA<(a9n{qvIO6xAeI(#-va7
zX)_m8sc46eB&v!|rJ|js&m=Z~=6xT*SmlBYqifOWNNj
zK63HO1-VA<(a9n{qvIO6xAeI>#-va7X)_m8sc46eB&v!|rJ|js&o#066Yu*7#wr(N
z7+s4_M`8C*x27F-W~Qqj)R=jPZv#``{kvC0J*M%SX#k=Ozq!{}N|pLfQZ^yz?h3ofWq(GDF+
zR27{{MLSEMcg2$Qk&9O@$Tf10P8RVQ9oNXcrO$g}LHcB$HgiFhigxHoqN?arD%x54
zyf@~gk6gTRL9UT|bh3!g=(tAiEq&e>Gtwvfw3!R4RJ21!5>-W~Qqj)R=lwAyedOYm
z3v!LzqmxB^M#nXBZ|U=a7?VEPr_EeYrJ@}=lBg;=m5O$jJ|B$DqrC4U7^_^6VRS7z
z9f>W_F^sOY^!ZS%NuLgAx8Q;*746WGL{-tLRJ617`EV>rAGvttf?Ol_=wuO}(Q%F3
zTl#z?7Nk%1X)_m8sc46eB&v!|rJ|js&qrfU`pCsA7vvhbM<kcY>2qsr{>b}2g0ad48AjKl(~;N$9mD8aOP^21n)K;_b_*`3Qqc|_NmLb`
zN<}+MpVP4-W~Qqj)R=QFV+edOYm3v!LzqmxB^M#nXBZ|U>d
zSdc#1r_EeYrJ@}=lBg;=m5O$jKKI6)^pT5KF32@o&@azU<D?k#=37-P~W`?Q%0s#LT?M-o*<
zr&7_*(&tQUe$V?pg0ad48AjKl(~;N$9mD8aOP?>tn)K;_b_*`3Qqc|_NmLb`N<}+M
zpRdG{^pT5KF32@o&@azU<D?k#=38Dr8X`?Q%0s#LT?M-o*7Kgo&@azU<D?k#
z+@q64d`8DLa&PJLtC*2K*{981P^F?BI+Ca=I+cocmOj6ZDd{5@uUwF8?XRkW3
c`+R2(4wtt)ZXfi5w3mPW?t`aCqnj`JA1Y|P3IG5A
literal 0
HcmV?d00001
diff --git a/tests/data/split.hdd/DiskDescriptor.xml b/tests/data/split.hdd/DiskDescriptor.xml
new file mode 100644
index 0000000..bdeaf83
--- /dev/null
+++ b/tests/data/split.hdd/DiskDescriptor.xml
@@ -0,0 +1,102 @@
+
+
+
+ 20971520
+ 40960
+ 4096
+ 512
+ 16
+ 32
+ 0
+
+ {00000000-0000-0000-0000-000000000000}
+
+
+
+ {d6e2bfb7-109e-4f6b-954c-6e2e7ae60d5a}
+ split
+
+ level2
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 1
+ 0
+
+
+
+
+ 0
+ 3989504
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Compressed
+ split.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+ 3989504
+ 7979008
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Compressed
+ split.hdd.1.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+ 7979008
+ 11968512
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Compressed
+ split.hdd.2.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+ 11968512
+ 15958016
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Compressed
+ split.hdd.3.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+ 15958016
+ 19947520
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Compressed
+ split.hdd.4.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+ 19947520
+ 20971520
+ 2048
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ Compressed
+ split.hdd.5.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds
+
+
+
+
+
+ {5fbaabe3-6958-40ff-92a7-860e329aab41}
+ {00000000-0000-0000-0000-000000000000}
+
+
+
diff --git a/tests/data/split.hdd/split.hdd b/tests/data/split.hdd/split.hdd
new file mode 100644
index 0000000..e69de29
diff --git a/tests/data/split.hdd/split.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz b/tests/data/split.hdd/split.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
new file mode 100644
index 0000000000000000000000000000000000000000..a1163e77aa05be14b295dc88b643e4a5fe7da8de
GIT binary patch
literal 3530
zcmb2|=3wBEGEQM&esj){ugO7x^#W&aS&^u=RJLA1qHl`u8Wonj!w+q^yfrx&E~(W2
zX*@Ij>}%t)`7&gIJfS?AweSQE!)WN>51qG=W(L!OoBOw{mu11HnD%lBt=X*GZPvOoFc>fZ08VYf
AE&u=k
literal 0
HcmV?d00001
diff --git a/tests/data/split.hdd/split.hdd.1.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz b/tests/data/split.hdd/split.hdd.1.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
new file mode 100644
index 0000000000000000000000000000000000000000..25f67c64b13d77431b6245b7e2ff0e164899eb7b
GIT binary patch
literal 3531
zcmb2|=3wBEGEQM&esj){ugO7x^#W&aS&^u=RJLA1qHl`u8Wonj!w+q^yfrx&E~(W2
zX*@Ij>}%t)`my8|3df2gC@<#x4f@MQZWF`lFrfmjwjehQ#78yVHgda(a^yc0{G-c)6Qt<
t;0pnKa-(Snf9SkDXvhmx!L;D)zlF7L74RvhgPeAZw)Bp`PzDAA1^_sY!zKU#
literal 0
HcmV?d00001
diff --git a/tests/data/split.hdd/split.hdd.2.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz b/tests/data/split.hdd/split.hdd.2.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
new file mode 100644
index 0000000000000000000000000000000000000000..db4722f0f6cd1602d8602104c1036f2442812fc0
GIT binary patch
literal 3534
zcmb2|=3wBEGEQM&esj){ugO7x^#W&aS&^u=RJLA1qHl`u8Wonj!w+q^yfrx&E~(W2
zX*@Ij>}%t)`qH9{fKaX3v37G416Ja9!upzC3}6fx&}%t)`qH9{oQXX3v37G416Ja9!v6b!2%R1A_qr0BVH9DF6Tf
literal 0
HcmV?d00001
diff --git a/tests/data/split.hdd/split.hdd.4.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz b/tests/data/split.hdd/split.hdd.4.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
new file mode 100644
index 0000000000000000000000000000000000000000..b0451d00660b66afbc9ae549c2e622b03a2d5899
GIT binary patch
literal 3534
zcmb2|=3wBEGEQM&esj){ugO7x^#W&aS&^u=RJLA1qHl`u8Wonj!w+q^yfrx&E~(W2
zX*@Ij>}%t)`qHp8P)@X3v37G416Ja9!s*wLVRgfx&Fmq`>F*KX$yDXf5e5ony2F!W+n=Ssq{TjE2r==->+hd~%~{XEb#1g#bRe
n(X@j
Date: Sun, 9 Apr 2023 22:24:03 +0200
Subject: [PATCH 2/6] Add support for Parallels PVS
---
dissect/hypervisor/__init__.py | 3 ++-
dissect/hypervisor/descriptor/pvs.py | 25 +++++++++++++++++++++++++
dissect/hypervisor/descriptor/vbox.py | 14 ++++++++++----
tests/test_pvs.py | 20 ++++++++++++++++++++
4 files changed, 57 insertions(+), 5 deletions(-)
create mode 100644 dissect/hypervisor/descriptor/pvs.py
create mode 100644 tests/test_pvs.py
diff --git a/dissect/hypervisor/__init__.py b/dissect/hypervisor/__init__.py
index 1ad7286..0da1051 100644
--- a/dissect/hypervisor/__init__.py
+++ b/dissect/hypervisor/__init__.py
@@ -1,5 +1,5 @@
from dissect.hypervisor.backup import vma, wim, xva
-from dissect.hypervisor.descriptor import hyperv, ovf, vbox, vmx
+from dissect.hypervisor.descriptor import hyperv, ovf, pvs, vbox, vmx
from dissect.hypervisor.disk import hdd, qcow2, vdi, vhd, vhdx, vmdk
from dissect.hypervisor.util import envelope, vmtar
@@ -8,6 +8,7 @@
"hdd",
"hyperv",
"ovf",
+ "pvs",
"qcow2",
"vbox",
"vdi",
diff --git a/dissect/hypervisor/descriptor/pvs.py b/dissect/hypervisor/descriptor/pvs.py
new file mode 100644
index 0000000..23c4a40
--- /dev/null
+++ b/dissect/hypervisor/descriptor/pvs.py
@@ -0,0 +1,25 @@
+from typing import IO, Iterator
+from xml.etree.ElementTree import Element
+
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ from xml.etree import ElementTree
+
+
+class PVS:
+ """Parallels VM settings file.
+
+ Args:
+ fh: The file-like object to a PVS file.
+ """
+
+ def __init__(self, fh: IO):
+ self._xml: Element = ElementTree.fromstring(fh.read())
+
+ def disks(self) -> Iterator[str]:
+ """Yield the disk file names."""
+ for hdd_elem in self._xml.iterfind(".//Hdd"):
+ system_name = hdd_elem.find("SystemName")
+ if system_name is not None:
+ yield system_name.text
diff --git a/dissect/hypervisor/descriptor/vbox.py b/dissect/hypervisor/descriptor/vbox.py
index 16b3a93..1dc1779 100644
--- a/dissect/hypervisor/descriptor/vbox.py
+++ b/dissect/hypervisor/descriptor/vbox.py
@@ -1,13 +1,19 @@
-from xml.etree import ElementTree
+from typing import IO, Iterator
+from xml.etree.ElementTree import Element
+
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ from xml.etree import ElementTree
class VBox:
VBOX_XML_NAMESPACE = "{http://www.virtualbox.org/}"
- def __init__(self, fh):
- self._xml = ElementTree.fromstring(fh.read())
+ def __init__(self, fh: IO):
+ self._xml: Element = ElementTree.fromstring(fh.read())
- def disks(self):
+ def disks(self) -> Iterator[str]:
for hdd_elem in self._xml.findall(
f".//{self.VBOX_XML_NAMESPACE}HardDisk[@location][@format='VDI'][@type='Normal']"
):
diff --git a/tests/test_pvs.py b/tests/test_pvs.py
new file mode 100644
index 0000000..2bec592
--- /dev/null
+++ b/tests/test_pvs.py
@@ -0,0 +1,20 @@
+from io import StringIO
+
+from dissect.hypervisor.descriptor.pvs import PVS
+
+
+def test_pvs():
+ xml = """
+
+
+
+
+ Fedora-0.hdd
+
+
+
+ """ # noqa: E501
+
+ with StringIO(xml.strip()) as fh:
+ pvs = PVS(fh)
+ assert next(pvs.disks()) == "Fedora-0.hdd"
From 01e8631c0bad6436c723f0c49ebe7956bcd42ae2 Mon Sep 17 00:00:00 2001
From: Schamper <1254028+Schamper@users.noreply.github.com>
Date: Tue, 18 Apr 2023 09:27:20 +0200
Subject: [PATCH 3/6] Address review comments
---
dissect/hypervisor/disk/hdd.py | 57 +++++++++++++++++-----------------
1 file changed, 28 insertions(+), 29 deletions(-)
diff --git a/dissect/hypervisor/disk/hdd.py b/dissect/hypervisor/disk/hdd.py
index a231c5f..dbd2140 100644
--- a/dissect/hypervisor/disk/hdd.py
+++ b/dissect/hypervisor/disk/hdd.py
@@ -19,6 +19,7 @@
from dissect.hypervisor.exceptions import InvalidHeaderError
DEFAULT_TOP_GUID = UUID("{5fbaabe3-6958-40ff-92a7-860e329aab41}")
+NULL_GUID = UUID("00000000-0000-0000-0000-000000000000")
class HDD:
@@ -89,7 +90,7 @@ def open(self, guid: Optional[Union[str, UUID]] = None) -> BinaryIO:
if guid is None:
guid = self.descriptor.snapshots.top_guid or DEFAULT_TOP_GUID
- chain = self.descriptor.get_chain(guid)
+ chain = self.descriptor.get_snapshot_chain(guid)
streams = []
for storage in self.descriptor.storage_data.storages:
@@ -124,7 +125,7 @@ def __init__(self, path: Path):
self.storage_data = StorageData.from_xml(self.xml.find("StorageData"))
self.snapshots = Snapshots.from_xml(self.xml.find("Snapshots"))
- def get_chain(self, guid: UUID) -> list[UUID]:
+ def get_snapshot_chain(self, guid: UUID) -> list[UUID]:
"""Return the snapshot chain for a given snapshot GUID.
Args:
@@ -133,7 +134,7 @@ def get_chain(self, guid: UUID) -> list[UUID]:
shot = self.snapshots.find_shot(guid)
chain = [shot.guid]
- while shot.parent != UUID("00000000-0000-0000-0000-000000000000"):
+ while shot.parent != NULL_GUID:
shot = self.snapshots.find_shot(shot.parent)
chain.append(shot.guid)
@@ -141,28 +142,35 @@ def get_chain(self, guid: UUID) -> list[UUID]:
@dataclass
-class StorageData:
- storages: list[Storage]
+class XMLEntry:
+ @classmethod
+ def from_xml(cls, element: Element) -> XMLEntry:
+ if element.tag != cls.__name__:
+ raise ValueError(f"Invalid {cls.__name__} XML element")
+ return cls._from_xml(element)
@classmethod
- def from_xml(cls, element: Element) -> StorageData:
- if element.tag != "StorageData":
- raise ValueError("Invalid StorageData XML element")
+ def _from_xml(cls, element: Element) -> XMLEntry:
+ raise NotImplementedError()
+
+
+@dataclass
+class StorageData(XMLEntry):
+ storages: list[Storage]
+ @classmethod
+ def _from_xml(cls, element: Element) -> StorageData:
return cls(list(map(Storage.from_xml, element.iterfind("Storage"))))
@dataclass
-class Storage:
+class Storage(XMLEntry):
start: int
end: int
images: list[Image]
@classmethod
- def from_xml(cls, element: Element) -> Storage:
- if element.tag != "Storage":
- raise ValueError("Invalid Storage XML element")
-
+ def _from_xml(cls, element: Element) -> Storage:
start = int(element.find("Start").text)
end = int(element.find("End").text)
images = list(map(Image.from_xml, element.iterfind("Image")))
@@ -186,16 +194,13 @@ def find_image(self, guid: UUID) -> Image:
@dataclass
-class Image:
+class Image(XMLEntry):
guid: UUID
type: str
file: str
@classmethod
- def from_xml(cls, element: Element) -> Image:
- if element.tag != "Image":
- raise ValueError("Invalid Image XML element")
-
+ def _from_xml(cls, element: Element) -> Image:
return cls(
UUID(element.find("GUID").text),
element.find("Type").text,
@@ -204,15 +209,12 @@ def from_xml(cls, element: Element) -> Image:
@dataclass
-class Snapshots:
+class Snapshots(XMLEntry):
top_guid: Optional[UUID]
shots: list[Shot]
@classmethod
- def from_xml(cls, element: Element) -> Snapshots:
- if element.tag != "Snapshots":
- raise ValueError("Invalid Snapshots XML element")
-
+ def _from_xml(cls, element: Element) -> Snapshots:
top_guid = element.find("TopGUID")
if top_guid:
top_guid = UUID(top_guid.text)
@@ -237,15 +239,12 @@ def find_shot(self, guid: UUID) -> Shot:
@dataclass
-class Shot:
+class Shot(XMLEntry):
guid: UUID
parent: UUID
@classmethod
- def from_xml(cls, element: Element) -> Shot:
- if element.tag != "Shot":
- raise ValueError("Invalid Shot XML element")
-
+ def _from_xml(cls, element: Element) -> Shot:
return cls(
UUID(element.find("GUID").text),
UUID(element.find("ParentGUID").text),
@@ -383,7 +382,7 @@ def _iter_runs(self, offset: int, length: int) -> Iterator[Tuple[int, int]]:
# First iteration
run_offset = read_offset
run_size = read_size
- elif read_offset == run_offset + run_size or (run_offset, read_offset) == (0, 0):
+ elif (read_offset == run_offset + run_size) or (run_offset, read_offset) == (0, 0):
# Consecutive (sparse) clusters
run_size += read_size
else:
From 7fb7087c4b79dc78b63665c3fa2933609a53c48e Mon Sep 17 00:00:00 2001
From: Schamper <1254028+Schamper@users.noreply.github.com>
Date: Tue, 18 Apr 2023 15:29:57 +0200
Subject: [PATCH 4/6] Add additional comments for
---
dissect/hypervisor/disk/hdd.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/dissect/hypervisor/disk/hdd.py b/dissect/hypervisor/disk/hdd.py
index dbd2140..98b4b80 100644
--- a/dissect/hypervisor/disk/hdd.py
+++ b/dissect/hypervisor/disk/hdd.py
@@ -55,18 +55,22 @@ def _open_image(self, path: Path) -> BinaryIO:
# If the absolute path does not exist, we're probably dealing with a HDD
# that's been copied or moved (e.g., uploaded or copied as evidence)
# Try a couple of common patterns to see if we can locate the file
+ #
+ # Example variables:
+ # root = /some/path/example.pvm/example.hdd/
+ # path = /other/path/absolute.pvm/absolute.hdd/absolute.ext
# File is in same HDD directory
- # root/example.pvm/example.hdd/absolute.ext
+ # candidate_path = /some/path/example.pvm/example.hdd/absolute.ext
candidate_path = root / filename
if not candidate_path.exists():
# File is in a separate HDD directory in parent (VM) directory
- # root/example.pvm/absolute.hdd/absolute.ext
+ # candidate_path = /some/path/example.pvm/absolute.hdd/absolute.ext
candidate_path = root.parent / path.parent.name / filename
if not candidate_path.exists():
# File is in .pvm directory in parent of parent directory (linked clones)
- # root/absolute.pvm/absolute.hdd/absolute.ext
+ # candidate_path = /some/path/absolute.pvm/absolute.hdd/absolute.ext
candidate_path = root.parent.parent / path.parent.parent.name / path.parent.name / filename
path = candidate_path
From 0e3da9e3220901bf3f071e40c0c0ed14c1a27c49 Mon Sep 17 00:00:00 2001
From: Schamper <1254028+Schamper@users.noreply.github.com>
Date: Tue, 18 Apr 2023 17:10:32 +0200
Subject: [PATCH 5/6] Remove some unused definitions
---
dissect/hypervisor/disk/c_hdd.py | 21 ++-------------------
1 file changed, 2 insertions(+), 19 deletions(-)
diff --git a/dissect/hypervisor/disk/c_hdd.py b/dissect/hypervisor/disk/c_hdd.py
index 5c04158..10eb252 100644
--- a/dissect/hypervisor/disk/c_hdd.py
+++ b/dissect/hypervisor/disk/c_hdd.py
@@ -1,9 +1,8 @@
+# Reference: https://src.openvz.org/projects/OVZ/repos/ploop/browse/include/ploop1_image.h
+
from dissect import cstruct
hdd_def = """
-/* Compressed disk (version 1) */
-#define PRL_IMAGE_COMPRESSED 2
-
/* Compressed disk v1 signature */
#define SIGNATURE_STRUCTURED_DISK_V1 b"WithoutFreeSpace"
@@ -13,23 +12,7 @@
/* Sign that the disk is in "using" state */
#define SIGNATURE_DISK_IN_USE 0x746F6E59
-/**
- * Compressed disk image flags
- */
-#define CIF_NoFlags 0x00000000 /* No any flags */
-#define CIF_Empty 0x00000001 /* No any data was written */
-#define CIF_FmtVersionConvert 0x00000002 /* Version Convert in progree */
-#define CIF_FlagsMask (CIF_Empty | CIF_FmtVersionConvert)
-#define CIF_Invalid 0xFFFFFFFF /* Invalid flag */
-
#define SECTOR_LOG 9
-#define DEF_CLUSTER_LOG 11 /* 1M cluster-block */
-#define DEF_CLUSTER (1 << (DEF_CLUSTER_LOG + SECTOR_LOG))
-
-/* Helpers to generate PVD-header based on requested bdsize */
-
-#define DEFAULT_HEADS_COUNT 16
-#define DEFAULT_SECTORS_COUNT 63
#define SECTOR_SIZE (1 << SECTOR_LOG)
struct pvd_header {
From c089576010513f2c6851fc501c4d7f6964b48686 Mon Sep 17 00:00:00 2001
From: Schamper <1254028+Schamper@users.noreply.github.com>
Date: Tue, 18 Apr 2023 17:17:11 +0200
Subject: [PATCH 6/6] Add additional references
---
dissect/hypervisor/disk/c_hdd.py | 5 ++++-
dissect/hypervisor/disk/hdd.py | 3 +++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/dissect/hypervisor/disk/c_hdd.py b/dissect/hypervisor/disk/c_hdd.py
index 10eb252..94fab02 100644
--- a/dissect/hypervisor/disk/c_hdd.py
+++ b/dissect/hypervisor/disk/c_hdd.py
@@ -1,4 +1,7 @@
-# Reference: https://src.openvz.org/projects/OVZ/repos/ploop/browse/include/ploop1_image.h
+# References:
+# - https://src.openvz.org/projects/OVZ/repos/ploop/browse/include/ploop1_image.h
+# - https://github.com/qemu/qemu/blob/master/docs/interop/parallels.txt
+
from dissect import cstruct
diff --git a/dissect/hypervisor/disk/hdd.py b/dissect/hypervisor/disk/hdd.py
index 98b4b80..b309959 100644
--- a/dissect/hypervisor/disk/hdd.py
+++ b/dissect/hypervisor/disk/hdd.py
@@ -118,6 +118,9 @@ def open(self, guid: Optional[Union[str, UUID]] = None) -> BinaryIO:
class Descriptor:
"""Helper class for working with ``DiskDescriptor.xml``.
+ References:
+ - https://github.com/qemu/qemu/blob/master/docs/interop/prl-xml.txt
+
Args:
path: The path to ``DiskDescriptor.xml``.
"""