dissect.hypervisor 3.17.dev1__tar.gz → 3.17.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. dissect_hypervisor-3.17.dev3/.git-blame-ignore-revs +6 -0
  2. {dissect_hypervisor-3.17.dev1/dissect.hypervisor.egg-info → dissect_hypervisor-3.17.dev3}/PKG-INFO +2 -2
  3. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/c_hyperv.py +2 -0
  4. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/hyperv.py +13 -15
  5. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/ovf.py +9 -5
  6. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/pvs.py +8 -3
  7. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/vbox.py +8 -3
  8. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/vmx.py +57 -47
  9. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_hdd.py +1 -1
  10. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_qcow2.py +4 -1
  11. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vdi.py +2 -0
  12. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vhd.py +2 -0
  13. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vhdx.py +2 -0
  14. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vmdk.py +2 -0
  15. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/hdd.py +13 -13
  16. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/qcow2.py +82 -72
  17. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vdi.py +5 -2
  18. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vhd.py +16 -13
  19. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vhdx.py +23 -17
  20. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vmdk.py +30 -30
  21. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/tools/envelope.py +5 -1
  22. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/util/envelope.py +12 -8
  23. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/util/vmtar.py +15 -8
  24. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3/dissect.hypervisor.egg-info}/PKG-INFO +2 -2
  25. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/SOURCES.txt +1 -0
  26. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/pyproject.toml +48 -5
  27. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/conftest.py +16 -11
  28. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_envelope.py +6 -3
  29. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_hdd.py +3 -1
  30. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_hyperv.py +6 -2
  31. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_ovf.py +3 -1
  32. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_pvs.py +3 -1
  33. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vbox.py +2 -0
  34. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vhd.py +8 -4
  35. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vhdx.py +18 -15
  36. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vmdk.py +8 -4
  37. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vmtar.py +5 -1
  38. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vmx.py +6 -2
  39. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tox.ini +4 -17
  40. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/COPYRIGHT +0 -0
  41. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/LICENSE +0 -0
  42. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/MANIFEST.in +0 -0
  43. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/README.md +0 -0
  44. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/__init__.py +0 -0
  45. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/__init__.py +0 -0
  46. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/__init__.py +0 -0
  47. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/exceptions.py +0 -0
  48. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/tools/__init__.py +0 -0
  49. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/util/__init__.py +0 -0
  50. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/dependency_links.txt +0 -0
  51. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/entry_points.txt +0 -0
  52. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/requires.txt +0 -0
  53. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/top_level.txt +0 -0
  54. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/setup.cfg +0 -0
  55. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/__init__.py +0 -0
  56. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/differencing.avhdx.gz +0 -0
  57. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/dynamic.vhd.gz +0 -0
  58. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/dynamic.vhdx.gz +0 -0
  59. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/encrypted.vmx +0 -0
  60. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/encryption.info +0 -0
  61. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/expanding.hdd/DiskDescriptor.xml +0 -0
  62. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/expanding.hdd/expanding.hdd +0 -0
  63. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/expanding.hdd/expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  64. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/fixed.vhd.gz +0 -0
  65. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/fixed.vhdx.gz +0 -0
  66. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/local.tgz.ve +0 -0
  67. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/plain.hdd/DiskDescriptor.xml +0 -0
  68. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/plain.hdd/plain.hdd +0 -0
  69. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/plain.hdd/plain.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  70. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/sesparse.vmdk.gz +0 -0
  71. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/DiskDescriptor.xml +0 -0
  72. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd +0 -0
  73. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  74. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.1.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  75. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.2.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  76. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.3.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  77. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.4.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  78. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.5.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  79. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/test.VMRS +0 -0
  80. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/test.vgz +0 -0
  81. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/test.vmcx +0 -0
  82. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/docs/Makefile +0 -0
  83. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/docs/conf.py +0 -0
  84. {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/docs/index.rst +0 -0
@@ -0,0 +1,6 @@
1
+ # Formatting commits. You can ignore them during git-blame with `--ignore-rev` or `--ignore-revs-file`.
2
+ #
3
+ # $ git config --add 'blame.ignoreRevsFile' '.git-blame-ignore-revs'
4
+ #
5
+ # Change linter to Ruff (#49)
6
+ ad8ca2f4d8a8bebedc6da8505a33b7b6e5c553d5
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: dissect.hypervisor
3
- Version: 3.17.dev1
3
+ Version: 3.17.dev3
4
4
  Summary: A Dissect module implementing parsers for various hypervisor disk, backup and configuration files
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dissect.cstruct import cstruct
2
4
 
3
5
  hyperv_def = """
@@ -3,8 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import struct
6
- from collections.abc import ItemsView, KeysView, ValuesView
7
- from typing import BinaryIO, Optional, Union
6
+ from typing import TYPE_CHECKING, BinaryIO
8
7
 
9
8
  from dissect.util.stream import RangeStream
10
9
 
@@ -16,6 +15,9 @@ from dissect.hypervisor.descriptor.c_hyperv import (
16
15
  )
17
16
  from dissect.hypervisor.exceptions import InvalidSignature
18
17
 
18
+ if TYPE_CHECKING:
19
+ from collections.abc import ItemsView, KeysView, ValuesView
20
+
19
21
 
20
22
  class HyperVFile:
21
23
  """HyperVFile implementation.
@@ -278,7 +280,7 @@ class HyperVStorageKeyTableEntry:
278
280
  return f"<HyperVStorageKeyTableEntry type={self.type} size={self.size}>"
279
281
 
280
282
  @property
281
- def parent(self) -> Optional[HyperVStorageKeyTableEntry]:
283
+ def parent(self) -> HyperVStorageKeyTableEntry | None:
282
284
  """Return the entry parent, if there is any.
283
285
 
284
286
  Requires that all key tables are loaded.
@@ -333,8 +335,8 @@ class HyperVStorageKeyTableEntry:
333
335
  file_object = self.get_file_object()
334
336
  # This memoryview has no purpose, only do it so the return value type is consistent
335
337
  return memoryview(file_object.read(size))
336
- else:
337
- return self.raw[self.header.data_offset :]
338
+
339
+ return self.raw[self.header.data_offset :]
338
340
 
339
341
  @property
340
342
  def key(self) -> str:
@@ -343,7 +345,7 @@ class HyperVStorageKeyTableEntry:
343
345
  return self.raw.tobytes()[: self.header.data_offset - 1].decode("utf-8")
344
346
 
345
347
  @property
346
- def value(self) -> Union[int, bytes, str]:
348
+ def value(self) -> int | bytes | str:
347
349
  """Return a Python native value for this entry."""
348
350
  data = self.data
349
351
 
@@ -369,6 +371,8 @@ class HyperVStorageKeyTableEntry:
369
371
  if self.type == KeyDataType.Bool:
370
372
  return struct.unpack("<I", data[:4])[0] != 0
371
373
 
374
+ raise TypeError(f"Unknown data type: {self.type}")
375
+
372
376
  @property
373
377
  def data_size(self) -> int:
374
378
  """Return the total amount of data bytes, including the key name.
@@ -427,10 +431,7 @@ class HyperVStorageKeyTableEntry:
427
431
 
428
432
  obj = {}
429
433
  for key, child in self.children.items():
430
- if child.type == KeyDataType.Node:
431
- value = child.as_dict()
432
- else:
433
- value = child.value
434
+ value = child.as_dict() if child.type == KeyDataType.Node else child.value
434
435
 
435
436
  obj[key] = value
436
437
 
@@ -466,13 +467,10 @@ class HyperVStorageFileObject:
466
467
  if n is not None and n < -1:
467
468
  raise ValueError("invalid number of bytes to read")
468
469
 
469
- if n == -1:
470
- read_length = self.size
471
- else:
472
- read_length = min(n, self.size)
470
+ read_length = self.size if n == -1 else min(n, self.size)
473
471
 
474
472
  self.file.fh.seek(self.offset)
475
473
  return self.file.fh.read(read_length)
476
474
 
477
- def open(self, size: Optional[int] = None) -> BinaryIO:
475
+ def open(self, size: int | None = None) -> BinaryIO:
478
476
  return RangeStream(self.file.fh, self.offset, size or self.size)
@@ -1,11 +1,16 @@
1
- from typing import Iterator, TextIO
2
- from xml.etree.ElementTree import Element
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Final, TextIO
3
4
 
4
5
  from defusedxml import ElementTree
5
6
 
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Iterator
9
+ from xml.etree.ElementTree import Element
10
+
6
11
 
7
12
  class OVF:
8
- NS = {
13
+ NS: Final[dict[str, str]] = {
9
14
  "ovf": "http://schemas.dmtf.org/ovf/envelope/1",
10
15
  "rasd": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData",
11
16
  }
@@ -34,8 +39,7 @@ class OVF:
34
39
  for disk in self.xml.findall(self.DISK_DRIVE_XPATH, self.NS):
35
40
  resource = disk.find("{{{rasd}}}HostResource".format(**self.NS))
36
41
  xpath = resource.text
37
- if xpath.startswith("ovf:"):
38
- xpath = xpath[4:]
42
+ xpath = xpath.removeprefix("ovf:")
39
43
 
40
44
  if xpath.startswith("/disk/"):
41
45
  disk_ref = xpath.split("/")[-1]
@@ -1,8 +1,13 @@
1
- from typing import IO, Iterator
2
- from xml.etree.ElementTree import Element
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, TextIO
3
4
 
4
5
  from defusedxml import ElementTree
5
6
 
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Iterator
9
+ from xml.etree.ElementTree import Element
10
+
6
11
 
7
12
  class PVS:
8
13
  """Parallels VM settings file.
@@ -11,7 +16,7 @@ class PVS:
11
16
  fh: The file-like object to a PVS file.
12
17
  """
13
18
 
14
- def __init__(self, fh: IO):
19
+ def __init__(self, fh: TextIO):
15
20
  self._xml: Element = ElementTree.fromstring(fh.read())
16
21
 
17
22
  def disks(self) -> Iterator[str]:
@@ -1,13 +1,18 @@
1
- from typing import IO, Iterator
2
- from xml.etree.ElementTree import Element
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, TextIO
3
4
 
4
5
  from defusedxml import ElementTree
5
6
 
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Iterator
9
+ from xml.etree.ElementTree import Element
10
+
6
11
 
7
12
  class VBox:
8
13
  VBOX_XML_NAMESPACE = "{http://www.virtualbox.org/}"
9
14
 
10
- def __init__(self, fh: IO):
15
+ def __init__(self, fh: TextIO):
11
16
  self._xml: Element = ElementTree.fromstring(fh.read())
12
17
 
13
18
  def disks(self) -> Iterator[str]:
@@ -7,11 +7,10 @@ import base64
7
7
  import hashlib
8
8
  import hmac
9
9
  import re
10
- from typing import Dict, List
11
10
  from urllib.parse import unquote
12
11
 
13
12
  try:
14
- import _pystandalone
13
+ import _pystandalone # type: ignore
15
14
 
16
15
  HAS_PYSTANDALONE = True
17
16
  except ImportError:
@@ -44,8 +43,8 @@ PASS2KEY_MAP = {
44
43
 
45
44
 
46
45
  class VMX:
47
- def __init__(self, vm_settings: Dict[str, str]):
48
- self.attr = vm_settings
46
+ def __init__(self, attr: dict[str, str]):
47
+ self.attr = attr
49
48
 
50
49
  @classmethod
51
50
  def parse(cls, string: str) -> VMX:
@@ -56,19 +55,28 @@ class VMX:
56
55
  def encrypted(self) -> bool:
57
56
  """Return whether this VMX is encrypted.
58
57
 
59
- Encrypted VMXs will have both a `encryption.keySafe` and `encryption.data` value.
60
- The `encryption.keySafe` is a string encoded `KeySafe`, which is made up of key locators.
58
+ Encrypted VMXs will have both a ``encryption.keySafe`` and ``encryption.data`` value.
59
+ The ``encryption.keySafe`` is a string encoded ``KeySafe``, which is made up of key locators.
61
60
 
62
61
  For example:
62
+
63
+ .. code-block:: none
64
+
63
65
  vmware:key/list/(pair/(phrase/phrase_id/phrase_content,hmac,data),pair/(.../...,...,...))
64
66
 
65
- A KeySafe must be a list of Pairs. Each Pair has a wrapped key, an HMAC type and some encrypted data.
67
+ A ``KeySafe`` must be a list of ``Pairs``. Each ``Pair`` has a wrapped key, an HMAC type and encrypted data.
66
68
  It's implementation specific how to unwrap a key. E.g. a phrase is just PBKDF2. The unwrapped key
67
- can be used to unlock the encrypted Pair data. This will contain the final encryption key to decrypt
68
- the data in `encryption.data`.
69
+ can be used to unlock the encrypted ``Pair`` data. This will contain the final encryption key to decrypt
70
+ the data in ``encryption.data``.
71
+
72
+ So, in summary, to unseal a ``KeySafe``:
69
73
 
70
- So, in summary, to unseal a KeySafe:
71
- Parse KeySafe -> iterate pairs -> unlock Pair -> unwrap key (e.g. Phrase) -> decrypt Pair data -> parse dict
74
+ - Parse ``KeySafe``
75
+ - Iterate pairs
76
+ - Unlock ``Pair``
77
+ - Unwrap key (e.g. ``Phrase``)
78
+ - Decrypt ``Pair`` data
79
+ - Parse dictionary
72
80
 
73
81
  The terms for unwrapping, unlocking and unsealing are taken from VMware.
74
82
  """
@@ -77,7 +85,7 @@ class VMX:
77
85
  def unlock_with_phrase(self, passphrase: str) -> None:
78
86
  """Unlock this VMX in-place with a passphrase if it's encrypted.
79
87
 
80
- This will load the KeySafe from the current dictionary and attempt to recover the encryption key
88
+ This will load the ``KeySafe`` from the current dictionary and attempt to recover the encryption key
81
89
  from it using the given passphrase. This key is used to decrypt the encrypted VMX data.
82
90
 
83
91
  The dictionary is updated in-place with the encrypted VMX data.
@@ -92,7 +100,7 @@ class VMX:
92
100
  decrypted = _decrypt_hmac(key, encrypted, mac)
93
101
  self.attr.update(**_parse_dictionary(decrypted.decode()))
94
102
 
95
- def disks(self) -> List[str]:
103
+ def disks(self) -> list[str]:
96
104
  """Return a list of paths to disk files"""
97
105
  dev_classes = ("scsi", "sata", "ide", "nvme")
98
106
  devices = {}
@@ -129,7 +137,7 @@ class VMX:
129
137
  return sorted(disk_files)
130
138
 
131
139
 
132
- def _parse_dictionary(string: str) -> Dict[str, str]:
140
+ def _parse_dictionary(string: str) -> dict[str, str]:
133
141
  """Parse a VMX dictionary."""
134
142
  dictionary = {}
135
143
 
@@ -148,11 +156,11 @@ def _parse_dictionary(string: str) -> Dict[str, str]:
148
156
 
149
157
 
150
158
  class KeySafe:
151
- def __init__(self, locators: List[Pair]):
159
+ def __init__(self, locators: list[Pair]):
152
160
  self.locators = locators
153
161
 
154
162
  def unseal_with_phrase(self, passphrase: str) -> bytes:
155
- """Unseal this KeySafe with a passphrase and return the decrypted key."""
163
+ """Unseal this ``KeySafe`` with a passphrase and return the decrypted key."""
156
164
  for locator in self.locators:
157
165
  if not locator.has_phrase():
158
166
  continue
@@ -170,7 +178,7 @@ class KeySafe:
170
178
 
171
179
  @classmethod
172
180
  def from_text(cls, text: str) -> KeySafe:
173
- """Parse a KeySafe from a string."""
181
+ """Parse a ``KeySafe`` from a string."""
174
182
 
175
183
  # Key safes are a list of key locators. It's a key locator string with a specific prefix
176
184
  identifier, _, remainder = text.partition("/")
@@ -179,41 +187,41 @@ class KeySafe:
179
187
 
180
188
  # First part must be a list of pairs
181
189
  locators = _parse_key_locator(remainder)
182
- if not isinstance(locators, list) and not all([isinstance(member, Pair) for member in locators]):
190
+ if not isinstance(locators, list) and not all(isinstance(member, Pair) for member in locators):
183
191
  raise ValueError("Invalid KeySafe string, not a list of pairs")
184
192
 
185
193
  return KeySafe(locators)
186
194
 
187
195
 
188
196
  class Pair:
189
- def __init__(self, wrapped_key, mac: str, data: bytes):
197
+ def __init__(self, wrapped_key: Phrase, mac: str, data: bytes):
190
198
  self.wrapped_key = wrapped_key
191
199
  self.mac = mac
192
200
  self.data = data
193
201
 
194
- def __repr__(self):
202
+ def __repr__(self) -> str:
195
203
  return f"<Pair wrapped_key={self.wrapped_key} mac={self.mac}>"
196
204
 
197
205
  def has_phrase(self) -> bool:
198
- """Return whether this Pair is a Phrase pair."""
206
+ """Return whether this ``Pair`` is a ``Phrase`` pair."""
199
207
  return isinstance(self.wrapped_key, Phrase)
200
208
 
201
209
  def _unlock(self, key: bytes) -> bytes:
202
- """Decrypt the data in this Pair."""
210
+ """Decrypt the data in this ``Pair``."""
203
211
  return _decrypt_hmac(key, self.data, self.mac)
204
212
 
205
213
  def unlock(self, *args, **kwargs) -> bytes:
206
- """Helper method to unlock this Pair for various wrapped keys.
214
+ """Helper method to unlock this ``Pair`` for various wrapped keys.
207
215
 
208
216
  Currently only supports `Phrase`.
209
217
  """
210
218
  if self.has_phrase():
211
219
  return self.unlock_with_phrase(*args, **kwargs)
212
- else:
213
- raise TypeError(f"Unable to unlock {self.key}")
220
+
221
+ raise TypeError(f"Unable to unlock {self.wrapped_key}")
214
222
 
215
223
  def unlock_with_phrase(self, passphrase: str) -> bytes:
216
- """Unlock this Pair with a passphrase and return the decrypted data."""
224
+ """Unlock this ``Pair`` with a passphrase and return the decrypted data."""
217
225
  if not self.has_phrase():
218
226
  raise TypeError("Pair doesn't have a phrase protected key")
219
227
 
@@ -229,13 +237,13 @@ class Phrase:
229
237
  self.rounds = rounds
230
238
  self.salt = salt
231
239
 
232
- def __repr__(self):
240
+ def __repr__(self) -> str:
233
241
  return f"<Phrase id={self.id} pass2key={self.pass2key} cipher={self.cipher} rounds={self.rounds}>"
234
242
 
235
243
  def unwrap(self, passphrase: str) -> bytes:
236
244
  """Unwrap/generate the encryption key for a given passphrase.
237
245
 
238
- VMware calls this unwrapping, but really it's a KDF with the properties of this Phrase.
246
+ VMware calls this unwrapping, but really it's a KDF with the properties of this ``Phrase``.
239
247
  """
240
248
  return hashlib.pbkdf2_hmac(
241
249
  PASS2KEY_MAP[self.pass2key],
@@ -246,13 +254,13 @@ class Phrase:
246
254
  )
247
255
 
248
256
 
249
- def _parse_key_locator(locator_string: str):
257
+ def _parse_key_locator(locator_string: str) -> Pair | Phrase | list[Pair | Phrase]:
250
258
  """Parse a key locator from a string.
251
259
 
252
- Key locators are string formatted data structures with a forward slash (/) separator. Each component is
253
- prefixed with a type, followed by that types' specific data. Values between separators are url encoded.
260
+ Key locators are string formatted data structures with a forward slash (``/``) separator. Each component is
261
+ prefixed with a type, followed by that types' specific data. Values between separators are URL encoded.
254
262
 
255
- Interally called `KeyLocator`.
263
+ Interally called ``KeyLocator``.
256
264
  """
257
265
 
258
266
  identifier, _, remainder = locator_string.partition("/")
@@ -261,7 +269,8 @@ def _parse_key_locator(locator_string: str):
261
269
  # Comma separated list in between braces
262
270
  # list/(member,member)
263
271
  return [_parse_key_locator(member) for member in _split_list(remainder)]
264
- elif identifier == "pair":
272
+
273
+ if identifier == "pair":
265
274
  # Comma separated tuple with 3 members
266
275
  # pair/(key data,mac type,encrypted data)
267
276
  members = _split_list(remainder)
@@ -270,7 +279,8 @@ def _parse_key_locator(locator_string: str):
270
279
  unquote(members[1]),
271
280
  base64.b64decode(unquote(members[2])),
272
281
  )
273
- elif identifier == "phrase":
282
+
283
+ if identifier == "phrase":
274
284
  # Serialized crypto dict, prefixed with an identifier
275
285
  # phrase/encoded id/encoded dict
276
286
  phrase_id, _, phrase_data = remainder.partition("/")
@@ -282,20 +292,19 @@ def _parse_key_locator(locator_string: str):
282
292
  int(crypto_dict["rounds"]),
283
293
  base64.b64decode(crypto_dict["salt"]),
284
294
  )
285
- else:
286
- # rawkey, ldap, script, role, fqid
287
- raise NotImplementedError(f"Not implemented keysafe identifier: {identifier}")
295
+
296
+ # rawkey, ldap, script, role, fqid
297
+ raise NotImplementedError(f"Not implemented keysafe identifier: {identifier}")
288
298
 
289
299
 
290
- def _split_list(list_string: str) -> List[str]:
300
+ def _split_list(value: str) -> list[str]:
291
301
  """Parse a key locator list from a string.
292
302
 
293
303
  Lists are wrapped by braces and separated by comma. They can contain nested lists/pairs,
294
304
  so we need to separate at the correct nest level.
295
305
  """
296
306
 
297
- match = re.match(r"\((.+)\)", list_string)
298
- if not match:
307
+ if not (match := re.match(r"\((.+)\)", value)):
299
308
  raise ValueError("Invalid list string")
300
309
 
301
310
  contents = match.group(1)
@@ -321,12 +330,12 @@ def _split_list(list_string: str) -> List[str]:
321
330
  return members
322
331
 
323
332
 
324
- def _parse_crypto_dict(dict_string: str) -> Dict[str, str]:
333
+ def _parse_crypto_dict(dict_string: str) -> dict[str, str]:
325
334
  """Parse a crypto dict from a string.
326
335
 
327
- Crypto dicts are encoded as `key=encoded_value:key=encoded_value`.
336
+ Crypto dicts are encoded as ``key=encoded_value:key=encoded_value``.
328
337
 
329
- Internally called `CryptoDict`.
338
+ Internally called ``CryptoDict``.
330
339
  """
331
340
 
332
341
  crypto_dict = {}
@@ -360,7 +369,7 @@ def _decrypt_hmac(key: bytes, data: bytes, digest: str) -> bytes:
360
369
  return decrypted
361
370
 
362
371
 
363
- def _create_cipher(key: bytes, iv: bytes):
372
+ def _create_cipher(key: bytes, iv: bytes) -> AES.CbcMode:
364
373
  """Create a cipher object.
365
374
 
366
375
  Dynamic based on the available crypto module.
@@ -377,7 +386,8 @@ def _create_cipher(key: bytes, iv: bytes):
377
386
  raise ValueError(f"Invalid key size: {len(key)}")
378
387
 
379
388
  return _pystandalone.cipher(cipher, key, iv)
380
- elif HAS_PYCRYPTODOME:
389
+
390
+ if HAS_PYCRYPTODOME:
381
391
  return AES.new(key, AES.MODE_CBC, iv=iv)
382
- else:
383
- raise RuntimeError("No crypto module available")
392
+
393
+ raise RuntimeError("No crypto module available")
@@ -1,7 +1,7 @@
1
1
  # References:
2
2
  # - https://src.openvz.org/projects/OVZ/repos/ploop/browse/include/ploop1_image.h
3
3
  # - https://github.com/qemu/qemu/blob/master/docs/interop/parallels.txt
4
-
4
+ from __future__ import annotations
5
5
 
6
6
  from dissect.cstruct import cstruct
7
7
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dissect.cstruct import cstruct
2
4
 
3
5
  qcow2_def = """
@@ -169,8 +171,9 @@ UNALLOCATED_SUBCLUSTER_TYPES = (
169
171
  )
170
172
 
171
173
 
172
- def ctz(value, size=32):
174
+ def ctz(value: int, size: int = 32) -> int:
173
175
  """Count the number of zero bits in an integer of a given size."""
174
176
  for i in range(size):
175
177
  if value & (1 << i):
176
178
  return i
179
+ return 0
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dissect.cstruct import cstruct
2
4
 
3
5
  # https://www.virtualbox.org/browser/vbox/trunk/src/VBox/Storage/VDICore.h
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dissect.cstruct import cstruct
2
4
 
3
5
  vhd_def = """
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from uuid import UUID
2
4
 
3
5
  from dissect.cstruct import cstruct
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import struct
2
4
 
3
5
  from dissect.cstruct import cstruct
@@ -4,9 +4,8 @@ from bisect import bisect_right
4
4
  from dataclasses import dataclass
5
5
  from functools import cached_property
6
6
  from pathlib import Path
7
- from typing import BinaryIO, Iterator, Optional, Tuple, Union
7
+ from typing import TYPE_CHECKING, BinaryIO
8
8
  from uuid import UUID
9
- from xml.etree.ElementTree import Element
10
9
 
11
10
  from defusedxml import ElementTree
12
11
  from dissect.util.stream import AlignedStream
@@ -14,6 +13,10 @@ from dissect.util.stream import AlignedStream
14
13
  from dissect.hypervisor.disk.c_hdd import SECTOR_SIZE, c_hdd
15
14
  from dissect.hypervisor.exceptions import InvalidHeaderError
16
15
 
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterator
18
+ from xml.etree.ElementTree import Element
19
+
17
20
  DEFAULT_TOP_GUID = UUID("{5fbaabe3-6958-40ff-92a7-860e329aab41}")
18
21
  NULL_GUID = UUID("00000000-0000-0000-0000-000000000000")
19
22
 
@@ -76,7 +79,7 @@ class HDD:
76
79
  # If the path is relative, it's always relative to the HDD root
77
80
  return (root / path).open("rb")
78
81
 
79
- def open(self, guid: Optional[Union[str, UUID]] = None) -> BinaryIO:
82
+ def open(self, guid: str | UUID | None = None) -> BinaryIO:
80
83
  """Open a stream for this HDD, optionally for a specific snapshot.
81
84
 
82
85
  If no snapshot GUID is provided, the "top" snapshot will be used.
@@ -154,7 +157,7 @@ class XMLEntry:
154
157
 
155
158
  @classmethod
156
159
  def _from_xml(cls, element: Element) -> XMLEntry:
157
- raise NotImplementedError()
160
+ raise NotImplementedError
158
161
 
159
162
 
160
163
  @dataclass
@@ -213,7 +216,7 @@ class Image(XMLEntry):
213
216
 
214
217
  @dataclass
215
218
  class Snapshots(XMLEntry):
216
- top_guid: Optional[UUID]
219
+ top_guid: UUID | None
217
220
  shots: list[Shot]
218
221
 
219
222
  @classmethod
@@ -307,7 +310,7 @@ class HDS(AlignedStream):
307
310
  parent: Optional file-like object for the parent HDS file.
308
311
  """
309
312
 
310
- def __init__(self, fh: BinaryIO, parent: Optional[BinaryIO] = None):
313
+ def __init__(self, fh: BinaryIO, parent: BinaryIO | None = None):
311
314
  self.fh = fh
312
315
  self.parent = parent
313
316
 
@@ -357,7 +360,7 @@ class HDS(AlignedStream):
357
360
 
358
361
  return b"".join(result)
359
362
 
360
- def _iter_runs(self, offset: int, length: int) -> Iterator[Tuple[int, int]]:
363
+ def _iter_runs(self, offset: int, length: int) -> Iterator[tuple[int, int]]:
361
364
  """Iterate optimized read runs for a given offset and read length.
362
365
 
363
366
  Args:
@@ -374,12 +377,9 @@ class HDS(AlignedStream):
374
377
  read_size = min(self.cluster_size - offset_in_cluster, length)
375
378
 
376
379
  bat_entry = bat[cluster_idx]
377
- if bat_entry == 0:
378
- # BAT entry of 0 means either a sparse or a parent read
379
- # Use 0 to denote a sparse run for now to make calculations easier
380
- read_offset = 0
381
- else:
382
- read_offset = (bat_entry * self._bat_multiplier * SECTOR_SIZE) + offset_in_cluster
380
+ # BAT entry of 0 means either a sparse or a parent read
381
+ # Use 0 to denote a sparse run for now to make calculations easier
382
+ read_offset = 0 if bat_entry == 0 else bat_entry * self._bat_multiplier * SECTOR_SIZE + offset_in_cluster
383
383
 
384
384
  if run_offset is None:
385
385
  # First iteration