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``. """