1+ from __future__ import annotations
2+
13import ctypes
24import io
35import logging
46import os
57import textwrap
68import zlib
79from bisect import bisect_right
10+ from dataclasses import dataclass
811from functools import lru_cache
912from 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+
401431class 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 )
0 commit comments