dissect.hypervisor 3.17.dev1__py3-none-any.whl → 3.17.dev3__py3-none-any.whl
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.
- dissect/hypervisor/descriptor/c_hyperv.py +2 -0
- dissect/hypervisor/descriptor/hyperv.py +13 -15
- dissect/hypervisor/descriptor/ovf.py +9 -5
- dissect/hypervisor/descriptor/pvs.py +8 -3
- dissect/hypervisor/descriptor/vbox.py +8 -3
- dissect/hypervisor/descriptor/vmx.py +57 -47
- dissect/hypervisor/disk/c_hdd.py +1 -1
- dissect/hypervisor/disk/c_qcow2.py +4 -1
- dissect/hypervisor/disk/c_vdi.py +2 -0
- dissect/hypervisor/disk/c_vhd.py +2 -0
- dissect/hypervisor/disk/c_vhdx.py +2 -0
- dissect/hypervisor/disk/c_vmdk.py +2 -0
- dissect/hypervisor/disk/hdd.py +13 -13
- dissect/hypervisor/disk/qcow2.py +82 -72
- dissect/hypervisor/disk/vdi.py +5 -2
- dissect/hypervisor/disk/vhd.py +16 -13
- dissect/hypervisor/disk/vhdx.py +23 -17
- dissect/hypervisor/disk/vmdk.py +30 -30
- dissect/hypervisor/tools/envelope.py +5 -1
- dissect/hypervisor/util/envelope.py +12 -8
- dissect/hypervisor/util/vmtar.py +15 -8
- {dissect.hypervisor-3.17.dev1.dist-info → dissect.hypervisor-3.17.dev3.dist-info}/METADATA +2 -2
- dissect.hypervisor-3.17.dev3.dist-info/RECORD +34 -0
- {dissect.hypervisor-3.17.dev1.dist-info → dissect.hypervisor-3.17.dev3.dist-info}/WHEEL +1 -1
- dissect.hypervisor-3.17.dev1.dist-info/RECORD +0 -34
- {dissect.hypervisor-3.17.dev1.dist-info → dissect.hypervisor-3.17.dev3.dist-info}/COPYRIGHT +0 -0
- {dissect.hypervisor-3.17.dev1.dist-info → dissect.hypervisor-3.17.dev3.dist-info}/LICENSE +0 -0
- {dissect.hypervisor-3.17.dev1.dist-info → dissect.hypervisor-3.17.dev3.dist-info}/entry_points.txt +0 -0
- {dissect.hypervisor-3.17.dev1.dist-info → dissect.hypervisor-3.17.dev3.dist-info}/top_level.txt +0 -0
dissect/hypervisor/disk/qcow2.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# References:
|
|
2
2
|
# - https://github.com/qemu/qemu/blob/master/block/qcow2.c
|
|
3
3
|
# - https://github.com/qemu/qemu/blob/master/docs/interop/qcow2.txt
|
|
4
|
+
from __future__ import annotations
|
|
4
5
|
|
|
5
6
|
import copy
|
|
6
7
|
import zlib
|
|
7
8
|
from functools import cached_property, lru_cache
|
|
8
9
|
from io import BytesIO
|
|
10
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
9
11
|
|
|
10
12
|
from dissect.util.stream import AlignedStream
|
|
11
13
|
|
|
@@ -21,6 +23,9 @@ from dissect.hypervisor.disk.c_qcow2 import (
|
|
|
21
23
|
)
|
|
22
24
|
from dissect.hypervisor.exceptions import Error, InvalidHeaderError
|
|
23
25
|
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
|
|
24
29
|
try:
|
|
25
30
|
import zstandard as zstd
|
|
26
31
|
|
|
@@ -45,7 +50,7 @@ class QCow2(AlignedStream):
|
|
|
45
50
|
in all null bytes being read.
|
|
46
51
|
"""
|
|
47
52
|
|
|
48
|
-
def __init__(self, fh, data_file=None, backing_file=None):
|
|
53
|
+
def __init__(self, fh: BinaryIO, data_file: BinaryIO | None = None, backing_file: BinaryIO | int | None = None):
|
|
49
54
|
self.fh = fh
|
|
50
55
|
|
|
51
56
|
self.header = c_qcow2.QCowHeader(fh)
|
|
@@ -120,7 +125,7 @@ class QCow2(AlignedStream):
|
|
|
120
125
|
|
|
121
126
|
super().__init__(self.header.size)
|
|
122
127
|
|
|
123
|
-
def _read_extensions(self):
|
|
128
|
+
def _read_extensions(self) -> None:
|
|
124
129
|
start_offset = self.header.header_length
|
|
125
130
|
end_offset = self.header.backing_file_offset or 1 << self.cluster_bits
|
|
126
131
|
|
|
@@ -135,7 +140,8 @@ class QCow2(AlignedStream):
|
|
|
135
140
|
|
|
136
141
|
if ext.magic == c_qcow2.QCOW2_EXT_MAGIC_END:
|
|
137
142
|
break
|
|
138
|
-
|
|
143
|
+
|
|
144
|
+
if ext.magic == c_qcow2.QCOW2_EXT_MAGIC_BACKING_FORMAT:
|
|
139
145
|
self.backing_format = self.fh.read(ext.len).decode().upper()
|
|
140
146
|
self.image_backing_format = self.backing_format.upper()
|
|
141
147
|
elif ext.magic == c_qcow2.QCOW2_EXT_MAGIC_FEATURE_TABLE:
|
|
@@ -153,7 +159,7 @@ class QCow2(AlignedStream):
|
|
|
153
159
|
offset += (ext.len + 7) & 0xFFFFFFF8
|
|
154
160
|
|
|
155
161
|
@cached_property
|
|
156
|
-
def snapshots(self):
|
|
162
|
+
def snapshots(self) -> list[QCow2Snapshot]:
|
|
157
163
|
snapshots = []
|
|
158
164
|
|
|
159
165
|
offset = self.header.snapshots_offset
|
|
@@ -164,46 +170,46 @@ class QCow2(AlignedStream):
|
|
|
164
170
|
return snapshots
|
|
165
171
|
|
|
166
172
|
@cached_property
|
|
167
|
-
def l1_table(self):
|
|
173
|
+
def l1_table(self) -> list[int]:
|
|
168
174
|
# L1 table is usually relatively small, it can be at most 32MB on PB or EB size disks
|
|
169
175
|
self.fh.seek(self.header.l1_table_offset)
|
|
170
176
|
return c_qcow2.uint64[self.header.l1_size](self.fh)
|
|
171
177
|
|
|
172
|
-
def l2_table(self, l2_offset):
|
|
178
|
+
def l2_table(self, l2_offset: int) -> L2Table:
|
|
173
179
|
return L2Table(self, l2_offset)
|
|
174
180
|
|
|
175
181
|
@property
|
|
176
|
-
def has_backing_file(self):
|
|
182
|
+
def has_backing_file(self) -> bool:
|
|
177
183
|
return self.backing_file is not None
|
|
178
184
|
|
|
179
185
|
@property
|
|
180
|
-
def has_data_file(self):
|
|
186
|
+
def has_data_file(self) -> bool:
|
|
181
187
|
return self.data_file != self.fh
|
|
182
188
|
|
|
183
189
|
@property
|
|
184
|
-
def has_subclusters(self):
|
|
190
|
+
def has_subclusters(self) -> bool:
|
|
185
191
|
return bool(self.header.incompatible_features & c_qcow2.QCOW2_INCOMPAT_EXTL2)
|
|
186
192
|
|
|
187
|
-
def _read(self, offset, length):
|
|
193
|
+
def _read(self, offset: int, length: int) -> bytes:
|
|
188
194
|
result = []
|
|
189
195
|
|
|
190
|
-
for sc_type,
|
|
196
|
+
for sc_type, read_offset, run_offset, run_length in self._yield_runs(offset, length):
|
|
191
197
|
unalloc_zeroed = sc_type in UNALLOCATED_SUBCLUSTER_TYPES and not self.has_backing_file
|
|
192
198
|
|
|
193
199
|
if sc_type in ZERO_SUBCLUSTER_TYPES or unalloc_zeroed:
|
|
194
200
|
result.append(b"\x00" * run_length)
|
|
195
201
|
elif sc_type in UNALLOCATED_SUBCLUSTER_TYPES and self.has_backing_file:
|
|
196
|
-
self.backing_file.seek(
|
|
202
|
+
self.backing_file.seek(read_offset)
|
|
197
203
|
result.append(self.backing_file.read(run_length))
|
|
198
204
|
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED:
|
|
199
|
-
result.append(self._read_compressed(run_offset,
|
|
205
|
+
result.append(self._read_compressed(run_offset, read_offset, run_length))
|
|
200
206
|
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL:
|
|
201
207
|
self.data_file.seek(run_offset)
|
|
202
208
|
result.append(self.data_file.read(run_length))
|
|
203
209
|
|
|
204
210
|
return b"".join(result)
|
|
205
211
|
|
|
206
|
-
def _read_compressed(self, cluster_descriptor, offset, length):
|
|
212
|
+
def _read_compressed(self, cluster_descriptor: int, offset: int, length: int) -> bytes:
|
|
207
213
|
offset_in_cluster = offset_into_cluster(self, offset)
|
|
208
214
|
coffset = cluster_descriptor & self.cluster_offset_mask
|
|
209
215
|
nb_csectors = ((cluster_descriptor >> self.csize_shift) & self.csize_mask) + 1
|
|
@@ -217,11 +223,12 @@ class QCow2(AlignedStream):
|
|
|
217
223
|
|
|
218
224
|
return decompressed[offset_in_cluster : offset_in_cluster + length]
|
|
219
225
|
|
|
220
|
-
def _decompress(self, buf):
|
|
226
|
+
def _decompress(self, buf: bytes) -> bytes:
|
|
221
227
|
if self.compression_type == c_qcow2.QCOW2_COMPRESSION_TYPE_ZLIB:
|
|
222
228
|
dctx = zlib.decompressobj(-12)
|
|
223
229
|
return dctx.decompress(buf, self.cluster_size)
|
|
224
|
-
|
|
230
|
+
|
|
231
|
+
if self.compression_type == c_qcow2.QCOW2_COMPRESSION_TYPE_ZSTD:
|
|
225
232
|
result = []
|
|
226
233
|
|
|
227
234
|
dctx = zstd.ZstdDecompressor()
|
|
@@ -232,10 +239,10 @@ class QCow2(AlignedStream):
|
|
|
232
239
|
break
|
|
233
240
|
result.append(chunk)
|
|
234
241
|
return b"".join(result)
|
|
235
|
-
else:
|
|
236
|
-
raise Error(f"Invalid compression type: {self.compression_type}")
|
|
237
242
|
|
|
238
|
-
|
|
243
|
+
raise Error(f"Invalid compression type: {self.compression_type}")
|
|
244
|
+
|
|
245
|
+
def _yield_runs(self, offset: int, length: int) -> Iterator[tuple[QCow2SubclusterType, int, int, int]]:
|
|
239
246
|
# reference: qcow2_get_host_offset
|
|
240
247
|
while length > 0:
|
|
241
248
|
sc_type = None
|
|
@@ -303,7 +310,7 @@ class QCow2(AlignedStream):
|
|
|
303
310
|
class L2Table:
|
|
304
311
|
"""Convenience class for accessing the L2 table."""
|
|
305
312
|
|
|
306
|
-
def __init__(self, qcow2, offset):
|
|
313
|
+
def __init__(self, qcow2: QCow2, offset: int):
|
|
307
314
|
self.qcow2 = qcow2
|
|
308
315
|
self.offset = offset
|
|
309
316
|
|
|
@@ -311,10 +318,10 @@ class L2Table:
|
|
|
311
318
|
self.qcow2.fh.seek(offset)
|
|
312
319
|
self._table = c_qcow2.uint64[l2_table_size](self.qcow2.fh)
|
|
313
320
|
|
|
314
|
-
def entry(self, idx):
|
|
321
|
+
def entry(self, idx: int) -> int:
|
|
315
322
|
return self._table[idx * self.qcow2._l2_entry_size // 8]
|
|
316
323
|
|
|
317
|
-
def bitmap(self, idx):
|
|
324
|
+
def bitmap(self, idx: int) -> int:
|
|
318
325
|
if self.qcow2.has_subclusters:
|
|
319
326
|
return self._table[(idx * self.qcow2._l2_entry_size // 8) + 1]
|
|
320
327
|
return 0
|
|
@@ -323,7 +330,7 @@ class L2Table:
|
|
|
323
330
|
class QCow2Snapshot:
|
|
324
331
|
"""Wrapper class for snapshot table entries."""
|
|
325
332
|
|
|
326
|
-
def __init__(self, qcow2, offset):
|
|
333
|
+
def __init__(self, qcow2: QCow2, offset: int):
|
|
327
334
|
self.qcow2 = qcow2
|
|
328
335
|
self.offset = offset
|
|
329
336
|
|
|
@@ -343,64 +350,65 @@ class QCow2Snapshot:
|
|
|
343
350
|
|
|
344
351
|
self.entry_size = self.qcow2.fh.tell() - offset
|
|
345
352
|
|
|
346
|
-
def open(self):
|
|
353
|
+
def open(self) -> QCow2:
|
|
347
354
|
disk = copy.copy(self.qcow2)
|
|
348
355
|
disk.l1_table = self.l1_table
|
|
349
356
|
disk.seek(0)
|
|
350
357
|
return disk
|
|
351
358
|
|
|
352
359
|
@cached_property
|
|
353
|
-
def l1_table(self):
|
|
360
|
+
def l1_table(self) -> list[int]:
|
|
354
361
|
# L1 table is usually relatively small, it can be at most 32MB on PB or EB size disks
|
|
355
362
|
self.qcow2.fh.seek(self.header.l1_table_offset)
|
|
356
363
|
return c_qcow2.uint64[self.header.l1_size](self.qcow2.fh)
|
|
357
364
|
|
|
358
365
|
|
|
359
|
-
def offset_into_cluster(qcow2, offset):
|
|
366
|
+
def offset_into_cluster(qcow2: QCow2, offset: int) -> int:
|
|
360
367
|
return offset & (qcow2.cluster_size - 1)
|
|
361
368
|
|
|
362
369
|
|
|
363
|
-
def offset_into_subcluster(qcow2, offset):
|
|
370
|
+
def offset_into_subcluster(qcow2: QCow2, offset: int) -> int:
|
|
364
371
|
return offset & (qcow2.subcluster_size - 1)
|
|
365
372
|
|
|
366
373
|
|
|
367
|
-
def size_to_clusters(qcow2, size):
|
|
374
|
+
def size_to_clusters(qcow2: QCow2, size: int) -> int:
|
|
368
375
|
return (size + (qcow2.cluster_size - 1)) >> qcow2.cluster_bits
|
|
369
376
|
|
|
370
377
|
|
|
371
|
-
def size_to_subclusters(qcow2, size):
|
|
378
|
+
def size_to_subclusters(qcow2: QCow2, size: int) -> int:
|
|
372
379
|
return (size + (qcow2.subcluster_size - 1)) >> qcow2.subcluster_bits
|
|
373
380
|
|
|
374
381
|
|
|
375
|
-
def offset_to_l1_index(qcow2, offset):
|
|
382
|
+
def offset_to_l1_index(qcow2: QCow2, offset: int) -> int:
|
|
376
383
|
return offset >> (qcow2.l2_bits + qcow2.cluster_bits)
|
|
377
384
|
|
|
378
385
|
|
|
379
|
-
def offset_to_l2_index(qcow2, offset):
|
|
386
|
+
def offset_to_l2_index(qcow2: QCow2, offset: int) -> int:
|
|
380
387
|
return (offset >> qcow2.cluster_bits) & (qcow2.l2_size - 1)
|
|
381
388
|
|
|
382
389
|
|
|
383
|
-
def offset_to_sc_index(qcow2, offset):
|
|
390
|
+
def offset_to_sc_index(qcow2: QCow2, offset: int) -> int:
|
|
384
391
|
return (offset >> qcow2.subcluster_bits) & (qcow2.subclusters_per_cluster - 1)
|
|
385
392
|
|
|
386
393
|
|
|
387
|
-
def get_cluster_type(qcow2, l2_entry):
|
|
394
|
+
def get_cluster_type(qcow2: QCow2, l2_entry: int) -> QCow2ClusterType:
|
|
388
395
|
if l2_entry & c_qcow2.QCOW_OFLAG_COMPRESSED:
|
|
389
396
|
return QCow2ClusterType.QCOW2_CLUSTER_COMPRESSED
|
|
390
|
-
|
|
397
|
+
|
|
398
|
+
if (l2_entry & c_qcow2.QCOW_OFLAG_ZERO) and not qcow2.has_subclusters:
|
|
391
399
|
if l2_entry & c_qcow2.L2E_OFFSET_MASK:
|
|
392
400
|
return QCow2ClusterType.QCOW2_CLUSTER_ZERO_ALLOC
|
|
393
401
|
return QCow2ClusterType.QCOW2_CLUSTER_ZERO_PLAIN
|
|
394
|
-
|
|
402
|
+
|
|
403
|
+
if not l2_entry & c_qcow2.L2E_OFFSET_MASK:
|
|
395
404
|
if qcow2.has_data_file and l2_entry & c_qcow2.QCOW_OFLAG_COPIED:
|
|
396
405
|
return QCow2ClusterType.QCOW2_CLUSTER_NORMAL
|
|
397
|
-
|
|
398
|
-
return QCow2ClusterType.QCOW2_CLUSTER_UNALLOCATED
|
|
399
|
-
else:
|
|
400
|
-
return QCow2ClusterType.QCOW2_CLUSTER_NORMAL
|
|
406
|
+
return QCow2ClusterType.QCOW2_CLUSTER_UNALLOCATED
|
|
401
407
|
|
|
408
|
+
return QCow2ClusterType.QCOW2_CLUSTER_NORMAL
|
|
402
409
|
|
|
403
|
-
|
|
410
|
+
|
|
411
|
+
def get_subcluster_type(qcow2: QCow2, l2_entry: int, l2_bitmap: int, sc_index: int) -> QCow2SubclusterType:
|
|
404
412
|
c_type = get_cluster_type(qcow2, l2_entry)
|
|
405
413
|
|
|
406
414
|
sc_alloc_mask = 1 << sc_index
|
|
@@ -409,40 +417,40 @@ def get_subcluster_type(qcow2, l2_entry, l2_bitmap, sc_index):
|
|
|
409
417
|
if qcow2.has_subclusters:
|
|
410
418
|
if c_type == QCow2ClusterType.QCOW2_CLUSTER_COMPRESSED:
|
|
411
419
|
return QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED
|
|
412
|
-
|
|
420
|
+
if c_type == QCow2ClusterType.QCOW2_CLUSTER_NORMAL:
|
|
413
421
|
if (l2_bitmap >> 32) & l2_bitmap:
|
|
414
422
|
return QCow2SubclusterType.QCOW2_SUBCLUSTER_INVALID
|
|
415
|
-
|
|
423
|
+
if l2_bitmap & sc_zero_mask: # QCOW_OFLAG_SUB_ZERO(sc_index)
|
|
416
424
|
return QCow2SubclusterType.QCOW2_SUBCLUSTER_ZERO_ALLOC
|
|
417
|
-
|
|
425
|
+
if l2_bitmap & sc_alloc_mask: # QCOW_OFLAG_SUB_ALLOC(sc_index)
|
|
418
426
|
return QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
elif c_type == QCow2ClusterType.QCOW2_CLUSTER_UNALLOCATED:
|
|
427
|
+
return QCow2SubclusterType.QCOW2_SUBCLUSTER_UNALLOCATED_ALLOC
|
|
428
|
+
if c_type == QCow2ClusterType.QCOW2_CLUSTER_UNALLOCATED:
|
|
422
429
|
if l2_bitmap & ((1 << 32) - 1):
|
|
423
430
|
return QCow2SubclusterType.QCOW2_SUBCLUSTER_INVALID
|
|
424
|
-
|
|
431
|
+
if l2_bitmap & sc_zero_mask: # QCOW_OFLAG_SUB_ZERO(sc_index)
|
|
425
432
|
return QCow2SubclusterType.QCOW2_SUBCLUSTER_ZERO_PLAIN
|
|
426
|
-
else:
|
|
427
|
-
return QCow2SubclusterType.QCOW2_SUBCLUSTER_UNALLOCATED_PLAIN
|
|
428
|
-
else:
|
|
429
|
-
raise Error(f"Invalid cluster type: {c_type}")
|
|
430
|
-
else:
|
|
431
|
-
if c_type == QCow2ClusterType.QCOW2_CLUSTER_COMPRESSED:
|
|
432
|
-
return QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED
|
|
433
|
-
elif c_type == QCow2ClusterType.QCOW2_CLUSTER_ZERO_PLAIN:
|
|
434
|
-
return QCow2SubclusterType.QCOW2_SUBCLUSTER_ZERO_PLAIN
|
|
435
|
-
elif c_type == QCow2ClusterType.QCOW2_CLUSTER_ZERO_ALLOC:
|
|
436
|
-
return QCow2SubclusterType.QCOW2_SUBCLUSTER_ZERO_ALLOC
|
|
437
|
-
elif c_type == QCow2ClusterType.QCOW2_CLUSTER_NORMAL:
|
|
438
|
-
return QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL
|
|
439
|
-
elif c_type == QCow2ClusterType.QCOW2_CLUSTER_UNALLOCATED:
|
|
440
433
|
return QCow2SubclusterType.QCOW2_SUBCLUSTER_UNALLOCATED_PLAIN
|
|
441
|
-
else:
|
|
442
|
-
raise Error(f"Invalid cluster type: {c_type}")
|
|
443
434
|
|
|
435
|
+
raise Error(f"Invalid cluster type: {c_type}")
|
|
444
436
|
|
|
445
|
-
|
|
437
|
+
if c_type == QCow2ClusterType.QCOW2_CLUSTER_COMPRESSED:
|
|
438
|
+
return QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED
|
|
439
|
+
if c_type == QCow2ClusterType.QCOW2_CLUSTER_ZERO_PLAIN:
|
|
440
|
+
return QCow2SubclusterType.QCOW2_SUBCLUSTER_ZERO_PLAIN
|
|
441
|
+
if c_type == QCow2ClusterType.QCOW2_CLUSTER_ZERO_ALLOC:
|
|
442
|
+
return QCow2SubclusterType.QCOW2_SUBCLUSTER_ZERO_ALLOC
|
|
443
|
+
if c_type == QCow2ClusterType.QCOW2_CLUSTER_NORMAL:
|
|
444
|
+
return QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL
|
|
445
|
+
if c_type == QCow2ClusterType.QCOW2_CLUSTER_UNALLOCATED:
|
|
446
|
+
return QCow2SubclusterType.QCOW2_SUBCLUSTER_UNALLOCATED_PLAIN
|
|
447
|
+
|
|
448
|
+
raise Error(f"Invalid cluster type: {c_type}")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def get_subcluster_range_type(
|
|
452
|
+
qcow2: QCow2, l2_entry: int, l2_bitmap: int, sc_from: int
|
|
453
|
+
) -> tuple[QCow2SubclusterType, int]:
|
|
446
454
|
sc_type = get_subcluster_type(qcow2, l2_entry, l2_bitmap, sc_from)
|
|
447
455
|
|
|
448
456
|
# No subclusters, so count the entire cluster
|
|
@@ -452,21 +460,23 @@ def get_subcluster_range_type(qcow2, l2_entry, l2_bitmap, sc_from):
|
|
|
452
460
|
sc_mask = (1 << sc_from) - 1
|
|
453
461
|
if sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL:
|
|
454
462
|
val = l2_bitmap | sc_mask # QCOW_OFLAG_SUB_ALLOC_RANGE(0, sc_from)
|
|
455
|
-
return ctz(val, 32) - sc_from
|
|
456
|
-
|
|
463
|
+
return sc_type, ctz(val, 32) - sc_from
|
|
464
|
+
if sc_type in ZERO_SUBCLUSTER_TYPES:
|
|
457
465
|
val = (l2_bitmap | sc_mask) >> 32 # QCOW_OFLAG_SUB_ZERO_RANGE(0, sc_from)
|
|
458
|
-
return ctz(val, 32) - sc_from
|
|
459
|
-
|
|
466
|
+
return sc_type, ctz(val, 32) - sc_from
|
|
467
|
+
if sc_type in UNALLOCATED_SUBCLUSTER_TYPES:
|
|
460
468
|
# We need to mask it with a 64bit mask because Python flips the sign bit
|
|
461
469
|
inv_mask = ~sc_mask & ((1 << 64) - 1) # ~QCOW_OFLAG_SUB_ALLOC_RANGE(0, sc_from)
|
|
462
470
|
|
|
463
471
|
val = ((l2_bitmap >> 32) | l2_bitmap) & inv_mask
|
|
464
|
-
return ctz(val, 32) - sc_from
|
|
465
|
-
|
|
466
|
-
|
|
472
|
+
return sc_type, ctz(val, 32) - sc_from
|
|
473
|
+
|
|
474
|
+
raise Error(f"Invalid subcluster type: {sc_type}")
|
|
467
475
|
|
|
468
476
|
|
|
469
|
-
def count_contiguous_subclusters(
|
|
477
|
+
def count_contiguous_subclusters(
|
|
478
|
+
qcow2: QCow2, nb_clusters: int, sc_index: int, l2_table: L2Table, l2_index: int
|
|
479
|
+
) -> int:
|
|
470
480
|
count = 0
|
|
471
481
|
expected_type = None
|
|
472
482
|
expected_offset = None
|
dissect/hypervisor/disk/vdi.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import array
|
|
4
|
+
from typing import BinaryIO
|
|
2
5
|
|
|
3
6
|
from dissect.util.stream import AlignedStream
|
|
4
7
|
|
|
@@ -7,7 +10,7 @@ from dissect.hypervisor.exceptions import Error
|
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class VDI(AlignedStream):
|
|
10
|
-
def __init__(self, fh, parent=None):
|
|
13
|
+
def __init__(self, fh: BinaryIO, parent: VDI | None = None):
|
|
11
14
|
self.fh = fh
|
|
12
15
|
self.parent = parent
|
|
13
16
|
self.header = c_vdi.HeaderDescriptor(fh)
|
|
@@ -32,7 +35,7 @@ class VDI(AlignedStream):
|
|
|
32
35
|
self.sector_size = self.header.SectorSize
|
|
33
36
|
super().__init__(size=self.header.DiskSize)
|
|
34
37
|
|
|
35
|
-
def _read(self, offset, length):
|
|
38
|
+
def _read(self, offset: int, length: int) -> bytes:
|
|
36
39
|
block_idx, block_offset = divmod(offset, self.block_size)
|
|
37
40
|
|
|
38
41
|
bytes_read = []
|
dissect/hypervisor/disk/vhd.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import io
|
|
2
4
|
import struct
|
|
3
5
|
from functools import lru_cache
|
|
6
|
+
from typing import BinaryIO
|
|
4
7
|
|
|
5
8
|
from dissect.util.stream import AlignedStream
|
|
6
9
|
|
|
7
10
|
from dissect.hypervisor.disk.c_vhd import SECTOR_SIZE, c_vhd
|
|
8
11
|
|
|
9
12
|
|
|
10
|
-
def read_footer(fh):
|
|
13
|
+
def read_footer(fh: BinaryIO) -> c_vhd.footer:
|
|
11
14
|
fh.seek(-512, io.SEEK_END)
|
|
12
15
|
footer = c_vhd.footer(fh)
|
|
13
16
|
if not footer.features & 0x00000002:
|
|
@@ -19,7 +22,7 @@ def read_footer(fh):
|
|
|
19
22
|
|
|
20
23
|
class VHD(AlignedStream):
|
|
21
24
|
# Note: split VHD files are currently unsupported.
|
|
22
|
-
def __init__(self, fh):
|
|
25
|
+
def __init__(self, fh: BinaryIO):
|
|
23
26
|
self.fh = fh
|
|
24
27
|
|
|
25
28
|
footer = read_footer(fh)
|
|
@@ -31,7 +34,7 @@ class VHD(AlignedStream):
|
|
|
31
34
|
|
|
32
35
|
super().__init__(self.disk.size)
|
|
33
36
|
|
|
34
|
-
def _read(self, offset, length):
|
|
37
|
+
def _read(self, offset: int, length: int) -> bytes:
|
|
35
38
|
sector = offset // SECTOR_SIZE
|
|
36
39
|
count = (length + SECTOR_SIZE - 1) // SECTOR_SIZE
|
|
37
40
|
|
|
@@ -39,23 +42,23 @@ class VHD(AlignedStream):
|
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
class Disk:
|
|
42
|
-
def __init__(self, fh, footer=None):
|
|
45
|
+
def __init__(self, fh: BinaryIO, footer: c_vhd.footer | None = None):
|
|
43
46
|
self.fh = fh
|
|
44
47
|
self.footer = footer if footer else read_footer(fh)
|
|
45
48
|
self.size = self.footer.current_size
|
|
46
49
|
|
|
47
|
-
def read_sectors(self, sector, count):
|
|
48
|
-
raise NotImplementedError
|
|
50
|
+
def read_sectors(self, sector: int, count: int) -> bytes:
|
|
51
|
+
raise NotImplementedError
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
class FixedDisk(Disk):
|
|
52
|
-
def read_sectors(self, sector, count):
|
|
55
|
+
def read_sectors(self, sector: int, count: int) -> bytes:
|
|
53
56
|
self.fh.seek(sector * SECTOR_SIZE)
|
|
54
57
|
return self.fh.read(count * SECTOR_SIZE)
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
class DynamicDisk(Disk):
|
|
58
|
-
def __init__(self, fh, footer=None):
|
|
61
|
+
def __init__(self, fh: BinaryIO, footer: c_vhd.footer | None = None):
|
|
59
62
|
super().__init__(fh, footer)
|
|
60
63
|
fh.seek(self.footer.data_offset)
|
|
61
64
|
self.header = c_vhd.dynamic_header(fh)
|
|
@@ -66,7 +69,7 @@ class DynamicDisk(Disk):
|
|
|
66
69
|
# Save bitmap size in sectors
|
|
67
70
|
self._sector_bitmap_size = ((self._sectors_per_block // 8) + SECTOR_SIZE - 1) // SECTOR_SIZE
|
|
68
71
|
|
|
69
|
-
def read_sectors(self, sector, count):
|
|
72
|
+
def read_sectors(self, sector: int, count: int) -> bytes:
|
|
70
73
|
result = []
|
|
71
74
|
while count > 0:
|
|
72
75
|
block, offset = divmod(sector, self._sectors_per_block)
|
|
@@ -96,17 +99,17 @@ class BlockAllocationTable:
|
|
|
96
99
|
|
|
97
100
|
ENTRY = struct.Struct(">I")
|
|
98
101
|
|
|
99
|
-
def __init__(self, fh, offset, max_entries):
|
|
102
|
+
def __init__(self, fh: BinaryIO, offset: int, max_entries: int):
|
|
100
103
|
self.fh = fh
|
|
101
104
|
self.offset = offset
|
|
102
105
|
self.max_entries = max_entries
|
|
103
106
|
|
|
104
107
|
self.get = lru_cache(4096)(self.get)
|
|
105
108
|
|
|
106
|
-
def get(self, block):
|
|
109
|
+
def get(self, block: int) -> int | None:
|
|
107
110
|
# This could be improved by caching the entire BAT (or chunks if too large)
|
|
108
111
|
if block + 1 > self.max_entries:
|
|
109
|
-
raise ValueError("Invalid block {} (max block is {
|
|
112
|
+
raise ValueError(f"Invalid block {block} (max block is {self.max_entries - 1})")
|
|
110
113
|
|
|
111
114
|
self.fh.seek(self.offset + block * 4)
|
|
112
115
|
sector_offset = self.ENTRY.unpack(self.fh.read(4))[0]
|
|
@@ -114,5 +117,5 @@ class BlockAllocationTable:
|
|
|
114
117
|
sector_offset = None
|
|
115
118
|
return sector_offset
|
|
116
119
|
|
|
117
|
-
def __getitem__(self, block):
|
|
120
|
+
def __getitem__(self, block: int) -> int | None:
|
|
118
121
|
return self.get(block)
|
dissect/hypervisor/disk/vhdx.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# References:
|
|
2
2
|
# - [MS-VHDX] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-vhdx/83e061f8-f6e2-4de1-91bd-5d518a43d477
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
import logging
|
|
5
7
|
import os
|
|
6
8
|
from functools import lru_cache
|
|
7
9
|
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Final
|
|
8
11
|
from uuid import UUID
|
|
9
12
|
|
|
10
13
|
from dissect.util.stream import AlignedStream
|
|
@@ -25,6 +28,9 @@ from dissect.hypervisor.disk.c_vhdx import (
|
|
|
25
28
|
)
|
|
26
29
|
from dissect.hypervisor.exceptions import InvalidSignature, InvalidVirtualDisk
|
|
27
30
|
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from collections.abc import Iterator
|
|
33
|
+
|
|
28
34
|
log = logging.getLogger(__name__)
|
|
29
35
|
log.setLevel(os.getenv("DISSECT_LOG_VHDX", "CRITICAL"))
|
|
30
36
|
|
|
@@ -38,7 +44,7 @@ class VHDX(AlignedStream):
|
|
|
38
44
|
the parent VHDX in the same directory, or the registered absolute directory.
|
|
39
45
|
"""
|
|
40
46
|
|
|
41
|
-
def __init__(self, fh):
|
|
47
|
+
def __init__(self, fh: BinaryIO | Path | str):
|
|
42
48
|
if hasattr(fh, "read"):
|
|
43
49
|
name = getattr(fh, "name", None)
|
|
44
50
|
path = Path(name) if name else None
|
|
@@ -85,7 +91,7 @@ class VHDX(AlignedStream):
|
|
|
85
91
|
self._chunk_ratio = ((2**23) * self.sector_size) // self.block_size
|
|
86
92
|
|
|
87
93
|
self.parent = None
|
|
88
|
-
self.parent_locator = None
|
|
94
|
+
self.parent_locator: ParentLocator = None
|
|
89
95
|
if self.has_parent:
|
|
90
96
|
self.parent_locator = self.metadata.get(PARENT_LOCATOR_GUID)
|
|
91
97
|
if self.parent_locator.type != VHDX_PARENT_LOCATOR_GUID:
|
|
@@ -97,7 +103,7 @@ class VHDX(AlignedStream):
|
|
|
97
103
|
|
|
98
104
|
super().__init__(self.size)
|
|
99
105
|
|
|
100
|
-
def read_sectors(self, sector, count):
|
|
106
|
+
def read_sectors(self, sector: int, count: int) -> bytes:
|
|
101
107
|
log.debug("VHDX::read_sectors(0x%x, 0x%x)", sector, count)
|
|
102
108
|
sectors_read = []
|
|
103
109
|
|
|
@@ -165,7 +171,7 @@ class VHDX(AlignedStream):
|
|
|
165
171
|
|
|
166
172
|
return b"".join(sectors_read)
|
|
167
173
|
|
|
168
|
-
def _read(self, offset, length):
|
|
174
|
+
def _read(self, offset: int, length: int) -> bytes:
|
|
169
175
|
sector = offset // self.sector_size
|
|
170
176
|
count = (length + self.sector_size - 1) // self.sector_size
|
|
171
177
|
|
|
@@ -173,7 +179,7 @@ class VHDX(AlignedStream):
|
|
|
173
179
|
|
|
174
180
|
|
|
175
181
|
class RegionTable:
|
|
176
|
-
def __init__(self, fh, offset):
|
|
182
|
+
def __init__(self, fh: BinaryIO, offset: int):
|
|
177
183
|
self.fh = fh
|
|
178
184
|
self.offset = offset
|
|
179
185
|
|
|
@@ -185,7 +191,7 @@ class RegionTable:
|
|
|
185
191
|
self.entries = c_vhdx.region_table_entry[self.header.entry_count](fh)
|
|
186
192
|
self.lookup = {UUID(bytes_le=e.guid): e for e in self.entries}
|
|
187
193
|
|
|
188
|
-
def get(self, guid, required=True):
|
|
194
|
+
def get(self, guid: UUID, required: bool = True) -> c_vhdx.region_table_entry | None:
|
|
189
195
|
data = self.lookup.get(guid)
|
|
190
196
|
if not data and required:
|
|
191
197
|
raise InvalidVirtualDisk(f"Missing required region: {guid}")
|
|
@@ -193,7 +199,7 @@ class RegionTable:
|
|
|
193
199
|
|
|
194
200
|
|
|
195
201
|
class BlockAllocationTable:
|
|
196
|
-
def __init__(self, vhdx, offset):
|
|
202
|
+
def __init__(self, vhdx: VHDX, offset: int):
|
|
197
203
|
self.vhdx = vhdx
|
|
198
204
|
self.offset = offset
|
|
199
205
|
self.chunk_ratio = vhdx._chunk_ratio
|
|
@@ -208,7 +214,7 @@ class BlockAllocationTable:
|
|
|
208
214
|
|
|
209
215
|
self.get = lru_cache(4096)(self.get)
|
|
210
216
|
|
|
211
|
-
def get(self, entry):
|
|
217
|
+
def get(self, entry: int) -> c_vhdx.bat_entry:
|
|
212
218
|
"""Get a BAT entry."""
|
|
213
219
|
if entry + 1 > self.entry_count:
|
|
214
220
|
raise ValueError(f"Invalid entry for BAT lookup: {entry} (max entry is {self.entry_count - 1})")
|
|
@@ -216,13 +222,13 @@ class BlockAllocationTable:
|
|
|
216
222
|
self.vhdx.fh.seek(self.offset + entry * 8)
|
|
217
223
|
return c_vhdx.bat_entry(self.vhdx.fh)
|
|
218
224
|
|
|
219
|
-
def pb(self, block):
|
|
225
|
+
def pb(self, block: int) -> c_vhdx.bat_entry:
|
|
220
226
|
"""Get a payload block entry for a given block."""
|
|
221
227
|
# Calculate how many interleaved sector bitmap entries there must be for this block
|
|
222
228
|
sb_entries = block // self.chunk_ratio
|
|
223
229
|
return self.get(block + sb_entries)
|
|
224
230
|
|
|
225
|
-
def sb(self, block):
|
|
231
|
+
def sb(self, block: int) -> c_vhdx.bat_entry:
|
|
226
232
|
"""Get a sector bitmap entry for a given block."""
|
|
227
233
|
# Calculate how many interleaved sector bitmap entries there must be for this block
|
|
228
234
|
num_sb = block // self.chunk_ratio
|
|
@@ -230,14 +236,14 @@ class BlockAllocationTable:
|
|
|
230
236
|
|
|
231
237
|
|
|
232
238
|
class ParentLocator:
|
|
233
|
-
def __init__(self, fh):
|
|
239
|
+
def __init__(self, fh: BinaryIO):
|
|
234
240
|
self.fh = fh
|
|
235
241
|
self.offset = fh.tell()
|
|
236
242
|
self.header = c_vhdx.parent_locator_header(fh)
|
|
237
243
|
self.type = UUID(bytes_le=self.header.locator_type)
|
|
238
244
|
self._entries = c_vhdx.parent_locator_entry[self.header.key_value_count](fh)
|
|
239
245
|
|
|
240
|
-
self.entries = {}
|
|
246
|
+
self.entries: dict[str, str] = {}
|
|
241
247
|
for entry in self._entries:
|
|
242
248
|
fh.seek(self.offset + entry.key_offset)
|
|
243
249
|
key = fh.read(entry.key_length).decode("utf-16-le")
|
|
@@ -248,7 +254,7 @@ class ParentLocator:
|
|
|
248
254
|
|
|
249
255
|
|
|
250
256
|
class MetadataTable:
|
|
251
|
-
METADATA_MAP = {
|
|
257
|
+
METADATA_MAP: Final[dict[UUID, Callable[[BinaryIO], Any]]] = {
|
|
252
258
|
FILE_PARAMETERS_GUID: c_vhdx.file_parameters,
|
|
253
259
|
VIRTUAL_DISK_SIZE_GUID: c_vhdx.virtual_disk_size,
|
|
254
260
|
VIRTUAL_DISK_ID_GUID: c_vhdx.virtual_disk_id,
|
|
@@ -257,7 +263,7 @@ class MetadataTable:
|
|
|
257
263
|
PARENT_LOCATOR_GUID: ParentLocator,
|
|
258
264
|
}
|
|
259
265
|
|
|
260
|
-
def __init__(self, fh, offset, length):
|
|
266
|
+
def __init__(self, fh: BinaryIO, offset: int, length: int):
|
|
261
267
|
self.fh = fh
|
|
262
268
|
self.offset = offset
|
|
263
269
|
self.length = length
|
|
@@ -277,14 +283,14 @@ class MetadataTable:
|
|
|
277
283
|
value = self.METADATA_MAP[item_id](fh)
|
|
278
284
|
self.lookup[item_id] = value
|
|
279
285
|
|
|
280
|
-
def get(self, guid, required=True):
|
|
286
|
+
def get(self, guid: UUID, required: bool = True) -> Any | None:
|
|
281
287
|
data = self.lookup.get(guid)
|
|
282
288
|
if not data and required:
|
|
283
289
|
raise InvalidVirtualDisk(f"Missing required region: {guid}")
|
|
284
290
|
return data
|
|
285
291
|
|
|
286
292
|
|
|
287
|
-
def _iter_partial_runs(bitmap, start_idx, length):
|
|
293
|
+
def _iter_partial_runs(bitmap: bytes, start_idx: int, length: int) -> Iterator[tuple[int, int]]:
|
|
288
294
|
current_type = (bitmap[0] & (1 << start_idx)) >> start_idx
|
|
289
295
|
current_count = 0
|
|
290
296
|
|
|
@@ -311,7 +317,7 @@ def _iter_partial_runs(bitmap, start_idx, length):
|
|
|
311
317
|
yield (current_type, current_count)
|
|
312
318
|
|
|
313
319
|
|
|
314
|
-
def open_parent(path, locator):
|
|
320
|
+
def open_parent(path: Path, locator: dict[str, str]) -> VHDX:
|
|
315
321
|
try:
|
|
316
322
|
filepath = path.joinpath(locator["relative_path"].replace("\\", "/"))
|
|
317
323
|
if not filepath.exists():
|