1+ from __future__ import annotations
2+
13import ctypes
24import io
35import logging
46import os
7+ import re
58import textwrap
69import zlib
710from bisect import bisect_right
11+ from dataclasses import dataclass
812from functools import lru_cache
913from pathlib import Path
1014
@@ -59,13 +63,13 @@ def __init__(self, fh):
5963 if self .descriptor .attr ["parentCID" ] != "ffffffff" :
6064 self .parent = open_parent (path .parent , self .descriptor .attr ["parentFileNameHint" ])
6165
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" )
66+ for extent in self .descriptor .extents :
67+ if extent . type in ["SPARSE" , "VMFSSPARSE" , "SESPARSE" ]:
68+ sdisk_fh = path .with_name (extent . filename ).open ("rb" )
6569 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 ))
70+ elif extent . type in ["VMFS" , "FLAT" ]:
71+ rdisk_fh = path .with_name (extent . filename ).open ("rb" )
72+ self .disks .append (RawDisk (rdisk_fh , extent . sectors * SECTOR_SIZE ))
6973
7074 elif magic in (COWD_MAGIC , VMDK_MAGIC , SESPARSE_MAGIC ):
7175 sparse_disk = SparseDisk (fh )
@@ -398,18 +402,69 @@ def __getattr__(self, attr):
398402 return getattr (self .hdr , attr )
399403
400404
405+ RE_EXTENT_DESCRIPTOR = re .compile (
406+ r"""
407+ ^
408+ (?P<access_mode>RW|RDONLY|NOACCESS)\s
409+ (?P<sectors>\d+)\s
410+ (?P<type>SPARSE|ZERO|FLAT|VMFS|VMFSSPARSE|VMFSRDM|VMFSRAW)
411+ (\s(?P<filename>\".+\"))?
412+ (\s(?P<start_sector>\d+))?
413+ (\s(?P<partition_uuid>\S+))?
414+ (\s(?P<device_identifier>\S+))?
415+ $
416+ """ ,
417+ re .VERBOSE ,
418+ )
419+
420+
421+ @dataclass
422+ class ExtentDescriptor :
423+ raw : str
424+ access_mode : str
425+ sectors : int
426+ type : str
427+ filename : str | None = None
428+ start_sector : int | None = None
429+ partition_uuid : str | None = None
430+ device_identifier : str | None = None
431+
432+ def __post_init__ (self ) -> None :
433+ self .sectors = int (self .sectors )
434+
435+ if self .filename :
436+ self .filename = self .filename .strip ('"' )
437+
438+ if self .start_sector :
439+ self .start_sector = int (self .start_sector )
440+
441+ def __repr__ (self ) -> str :
442+ return f"<ExtentDescriptor { self .raw } >"
443+
444+ def __str__ (self ) -> str :
445+ return self .raw
446+
447+
401448class DiskDescriptor :
402- def __init__ (self , attr , extents , disk_db , sectors , raw_config = None ):
449+ def __init__ (
450+ self , attr : dict , extents : list [ExtentDescriptor ], disk_db : dict , sectors : int , raw_config : str | None = None
451+ ):
403452 self .attr = attr
404453 self .extents = extents
405454 self .ddb = disk_db
406455 self .sectors = sectors
407456 self .raw = raw_config
408457
409458 @classmethod
410- def parse (cls , vmdk_config ):
459+ def parse (cls , vmdk_config : str ) -> DiskDescriptor :
460+ """Return :class:`DiskDescriptor` based on the provided ``vmdk_config``.
461+
462+ Resources:
463+ - https://github.com/libyal/libvmdk/blob/main/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc
464+ """ # noqa: E501
465+
411466 descriptor_settings = {}
412- extents = []
467+ extents : list [ ExtentDescriptor ] = []
413468 disk_db = {}
414469 sectors = 0
415470
@@ -420,11 +475,15 @@ def parse(cls, vmdk_config):
420475 continue
421476
422477 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 ])
478+ match = RE_EXTENT_DESCRIPTOR .search (line )
479+
480+ if not match :
481+ log .warning ("Unexpected ExtentDescriptor format in vmdk config: %s, ignoring" , line )
482+ continue
483+
484+ extent = ExtentDescriptor (raw = line , ** match .groupdict ())
485+ sectors += extent .sectors
486+ extents .append (extent )
428487 continue
429488
430489 setting , _ , value = line .partition ("=" )
@@ -438,35 +497,33 @@ def parse(cls, vmdk_config):
438497
439498 return cls (descriptor_settings , extents , disk_db , sectors , vmdk_config )
440499
441- def __str__ (self ):
442- str_template = """\
443- # Disk DescriptorFile
444- version=1
445- {}
500+ def __str__ (self ) -> str :
501+ str_template = textwrap .dedent (
502+ """\
503+ # Disk DescriptorFile
504+ version=1
505+ {}
446506
447- # Extent Description
448- {}
507+ # Extent Description
508+ {}
449509
450- # The Disk Data Base
451- #DDB
510+ # The Disk Data Base
511+ #DDB
512+
513+ {}"""
514+ )
452515
453- {}"""
454- str_template = textwrap .dedent (str_template )
455516 descriptor_settings = []
456517 for setting , value in self .attr .items ():
457- if setting == "version" :
458- continue
459- descriptor_settings .append ("{}={}" .format (setting , value ))
518+ if setting != "version" :
519+ descriptor_settings .append (f"{ setting } ={ value } " )
460520 descriptor_settings = "\n " .join (descriptor_settings )
461521
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 )
522+ extents = "\n " .join (map (str , self .extents ))
466523
467524 disk_db = []
468525 for setting , value in self .ddb .items ():
469- disk_db .append ('{ } = "{}"'. format ( setting , value ) )
526+ disk_db .append (f' { setting } = "{ value } "' )
470527 disk_db = "\n " .join (disk_db )
471528
472529 return str_template .format (descriptor_settings , extents , disk_db )
0 commit comments