dissect.hypervisor 3.18.dev1__py3-none-any.whl → 3.19.dev2__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/disk/c_qcow2.py +2 -5
- dissect/hypervisor/disk/c_qcow2.pyi +190 -0
- dissect/hypervisor/disk/qcow2.py +198 -124
- {dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/METADATA +1 -1
- {dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/RECORD +10 -9
- {dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/WHEEL +1 -1
- {dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/entry_points.txt +0 -0
- {dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/COPYRIGHT +0 -0
- {dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/LICENSE +0 -0
- {dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/top_level.txt +0 -0
|
@@ -171,9 +171,6 @@ UNALLOCATED_SUBCLUSTER_TYPES = (
|
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
def ctz(value: int
|
|
174
|
+
def ctz(value: int) -> int:
|
|
175
175
|
"""Count the number of zero bits in an integer of a given size."""
|
|
176
|
-
|
|
177
|
-
if value & (1 << i):
|
|
178
|
-
return i
|
|
179
|
-
return 0
|
|
176
|
+
return (value & -value).bit_length() - 1 if value else 0
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Generated by cstruct-stubgen
|
|
2
|
+
from typing import BinaryIO, Literal, overload
|
|
3
|
+
|
|
4
|
+
import dissect.cstruct as __cs__
|
|
5
|
+
from typing_extensions import TypeAlias
|
|
6
|
+
|
|
7
|
+
class _c_qcow2(__cs__.cstruct):
|
|
8
|
+
MIN_CLUSTER_BITS: Literal[9] = ...
|
|
9
|
+
MAX_CLUSTER_BITS: Literal[21] = ...
|
|
10
|
+
QCOW2_COMPRESSED_SECTOR_SIZE: Literal[512] = ...
|
|
11
|
+
QCOW2_COMPRESSION_TYPE_ZLIB: Literal[0] = ...
|
|
12
|
+
QCOW2_COMPRESSION_TYPE_ZSTD: Literal[1] = ...
|
|
13
|
+
L1E_SIZE: Literal[8] = ...
|
|
14
|
+
L2E_SIZE_NORMAL: Literal[8] = ...
|
|
15
|
+
L2E_SIZE_EXTENDED: Literal[16] = ...
|
|
16
|
+
L1E_OFFSET_MASK: Literal[72057594037927424] = ...
|
|
17
|
+
L2E_OFFSET_MASK: Literal[72057594037927424] = ...
|
|
18
|
+
L2E_COMPRESSED_OFFSET_SIZE_MASK: Literal[4611686018427387903] = ...
|
|
19
|
+
QCOW_OFLAG_COPIED: Literal[9223372036854775808] = ...
|
|
20
|
+
QCOW_OFLAG_COMPRESSED: Literal[4611686018427387904] = ...
|
|
21
|
+
QCOW_OFLAG_ZERO: Literal[1] = ...
|
|
22
|
+
QCOW_EXTL2_SUBCLUSTERS_PER_CLUSTER: Literal[32] = ...
|
|
23
|
+
QCOW2_INCOMPAT_DIRTY_BITNR: Literal[0] = ...
|
|
24
|
+
QCOW2_INCOMPAT_CORRUPT_BITNR: Literal[1] = ...
|
|
25
|
+
QCOW2_INCOMPAT_DATA_FILE_BITNR: Literal[2] = ...
|
|
26
|
+
QCOW2_INCOMPAT_COMPRESSION_BITNR: Literal[3] = ...
|
|
27
|
+
QCOW2_INCOMPAT_EXTL2_BITNR: Literal[4] = ...
|
|
28
|
+
QCOW2_INCOMPAT_DIRTY: Literal[1] = ...
|
|
29
|
+
QCOW2_INCOMPAT_CORRUPT: Literal[2] = ...
|
|
30
|
+
QCOW2_INCOMPAT_DATA_FILE: Literal[4] = ...
|
|
31
|
+
QCOW2_INCOMPAT_COMPRESSION: Literal[8] = ...
|
|
32
|
+
QCOW2_INCOMPAT_EXTL2: Literal[16] = ...
|
|
33
|
+
QCOW2_EXT_MAGIC_END: Literal[0] = ...
|
|
34
|
+
QCOW2_EXT_MAGIC_BACKING_FORMAT: Literal[3799591626] = ...
|
|
35
|
+
QCOW2_EXT_MAGIC_FEATURE_TABLE: Literal[1745090647] = ...
|
|
36
|
+
QCOW2_EXT_MAGIC_CRYPTO_HEADER: Literal[87539319] = ...
|
|
37
|
+
QCOW2_EXT_MAGIC_BITMAPS: Literal[595929205] = ...
|
|
38
|
+
QCOW2_EXT_MAGIC_DATA_FILE: Literal[1145132097] = ...
|
|
39
|
+
class QCowHeader(__cs__.Structure):
|
|
40
|
+
magic: _c_qcow2.uint32
|
|
41
|
+
version: _c_qcow2.uint32
|
|
42
|
+
backing_file_offset: _c_qcow2.uint64
|
|
43
|
+
backing_file_size: _c_qcow2.uint32
|
|
44
|
+
cluster_bits: _c_qcow2.uint32
|
|
45
|
+
size: _c_qcow2.uint64
|
|
46
|
+
crypt_method: _c_qcow2.uint32
|
|
47
|
+
l1_size: _c_qcow2.uint32
|
|
48
|
+
l1_table_offset: _c_qcow2.uint64
|
|
49
|
+
refcount_table_offset: _c_qcow2.uint64
|
|
50
|
+
refcount_table_clusters: _c_qcow2.uint32
|
|
51
|
+
nb_snapshots: _c_qcow2.uint32
|
|
52
|
+
snapshots_offset: _c_qcow2.uint64
|
|
53
|
+
incompatible_features: _c_qcow2.uint64
|
|
54
|
+
compatible_features: _c_qcow2.uint64
|
|
55
|
+
autoclear_features: _c_qcow2.uint64
|
|
56
|
+
refcount_order: _c_qcow2.uint32
|
|
57
|
+
header_length: _c_qcow2.uint32
|
|
58
|
+
compression_type: _c_qcow2.uint8
|
|
59
|
+
padding: __cs__.Array[_c_qcow2.uint8]
|
|
60
|
+
@overload
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
magic: _c_qcow2.uint32 | None = ...,
|
|
64
|
+
version: _c_qcow2.uint32 | None = ...,
|
|
65
|
+
backing_file_offset: _c_qcow2.uint64 | None = ...,
|
|
66
|
+
backing_file_size: _c_qcow2.uint32 | None = ...,
|
|
67
|
+
cluster_bits: _c_qcow2.uint32 | None = ...,
|
|
68
|
+
size: _c_qcow2.uint64 | None = ...,
|
|
69
|
+
crypt_method: _c_qcow2.uint32 | None = ...,
|
|
70
|
+
l1_size: _c_qcow2.uint32 | None = ...,
|
|
71
|
+
l1_table_offset: _c_qcow2.uint64 | None = ...,
|
|
72
|
+
refcount_table_offset: _c_qcow2.uint64 | None = ...,
|
|
73
|
+
refcount_table_clusters: _c_qcow2.uint32 | None = ...,
|
|
74
|
+
nb_snapshots: _c_qcow2.uint32 | None = ...,
|
|
75
|
+
snapshots_offset: _c_qcow2.uint64 | None = ...,
|
|
76
|
+
incompatible_features: _c_qcow2.uint64 | None = ...,
|
|
77
|
+
compatible_features: _c_qcow2.uint64 | None = ...,
|
|
78
|
+
autoclear_features: _c_qcow2.uint64 | None = ...,
|
|
79
|
+
refcount_order: _c_qcow2.uint32 | None = ...,
|
|
80
|
+
header_length: _c_qcow2.uint32 | None = ...,
|
|
81
|
+
compression_type: _c_qcow2.uint8 | None = ...,
|
|
82
|
+
padding: __cs__.Array[_c_qcow2.uint8] | None = ...,
|
|
83
|
+
): ...
|
|
84
|
+
@overload
|
|
85
|
+
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
|
|
86
|
+
|
|
87
|
+
class QCowExtension(__cs__.Structure):
|
|
88
|
+
magic: _c_qcow2.uint32
|
|
89
|
+
len: _c_qcow2.uint32
|
|
90
|
+
@overload
|
|
91
|
+
def __init__(self, magic: _c_qcow2.uint32 | None = ..., len: _c_qcow2.uint32 | None = ...): ...
|
|
92
|
+
@overload
|
|
93
|
+
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
|
|
94
|
+
|
|
95
|
+
class Qcow2CryptoHeaderExtension(__cs__.Structure):
|
|
96
|
+
offset: _c_qcow2.uint64
|
|
97
|
+
length: _c_qcow2.uint64
|
|
98
|
+
@overload
|
|
99
|
+
def __init__(self, offset: _c_qcow2.uint64 | None = ..., length: _c_qcow2.uint64 | None = ...): ...
|
|
100
|
+
@overload
|
|
101
|
+
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
|
|
102
|
+
|
|
103
|
+
class Qcow2BitmapHeaderExt(__cs__.Structure):
|
|
104
|
+
nb_bitmaps: _c_qcow2.uint32
|
|
105
|
+
reserved32: _c_qcow2.uint32
|
|
106
|
+
bitmap_directory_size: _c_qcow2.uint64
|
|
107
|
+
bitmap_directory_offset: _c_qcow2.uint64
|
|
108
|
+
@overload
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
nb_bitmaps: _c_qcow2.uint32 | None = ...,
|
|
112
|
+
reserved32: _c_qcow2.uint32 | None = ...,
|
|
113
|
+
bitmap_directory_size: _c_qcow2.uint64 | None = ...,
|
|
114
|
+
bitmap_directory_offset: _c_qcow2.uint64 | None = ...,
|
|
115
|
+
): ...
|
|
116
|
+
@overload
|
|
117
|
+
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
|
|
118
|
+
|
|
119
|
+
class QCowSnapshotHeader(__cs__.Structure):
|
|
120
|
+
l1_table_offset: _c_qcow2.uint64
|
|
121
|
+
l1_size: _c_qcow2.uint32
|
|
122
|
+
id_str_size: _c_qcow2.uint16
|
|
123
|
+
name_size: _c_qcow2.uint16
|
|
124
|
+
date_sec: _c_qcow2.uint32
|
|
125
|
+
date_nsec: _c_qcow2.uint32
|
|
126
|
+
vm_clock_nsec: _c_qcow2.uint64
|
|
127
|
+
vm_state_size: _c_qcow2.uint32
|
|
128
|
+
extra_data_size: _c_qcow2.uint32
|
|
129
|
+
@overload
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
l1_table_offset: _c_qcow2.uint64 | None = ...,
|
|
133
|
+
l1_size: _c_qcow2.uint32 | None = ...,
|
|
134
|
+
id_str_size: _c_qcow2.uint16 | None = ...,
|
|
135
|
+
name_size: _c_qcow2.uint16 | None = ...,
|
|
136
|
+
date_sec: _c_qcow2.uint32 | None = ...,
|
|
137
|
+
date_nsec: _c_qcow2.uint32 | None = ...,
|
|
138
|
+
vm_clock_nsec: _c_qcow2.uint64 | None = ...,
|
|
139
|
+
vm_state_size: _c_qcow2.uint32 | None = ...,
|
|
140
|
+
extra_data_size: _c_qcow2.uint32 | None = ...,
|
|
141
|
+
): ...
|
|
142
|
+
@overload
|
|
143
|
+
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
|
|
144
|
+
|
|
145
|
+
class QCowSnapshotExtraData(__cs__.Structure):
|
|
146
|
+
vm_state_size_large: _c_qcow2.uint64
|
|
147
|
+
disk_size: _c_qcow2.uint64
|
|
148
|
+
icount: _c_qcow2.uint64
|
|
149
|
+
@overload
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
vm_state_size_large: _c_qcow2.uint64 | None = ...,
|
|
153
|
+
disk_size: _c_qcow2.uint64 | None = ...,
|
|
154
|
+
icount: _c_qcow2.uint64 | None = ...,
|
|
155
|
+
): ...
|
|
156
|
+
@overload
|
|
157
|
+
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
|
|
158
|
+
|
|
159
|
+
class QCow2ClusterType(__cs__.Enum):
|
|
160
|
+
QCOW2_CLUSTER_UNALLOCATED = ...
|
|
161
|
+
QCOW2_CLUSTER_ZERO_PLAIN = ...
|
|
162
|
+
QCOW2_CLUSTER_ZERO_ALLOC = ...
|
|
163
|
+
QCOW2_CLUSTER_NORMAL = ...
|
|
164
|
+
QCOW2_CLUSTER_COMPRESSED = ...
|
|
165
|
+
|
|
166
|
+
class QCow2SubclusterType(__cs__.Enum):
|
|
167
|
+
QCOW2_SUBCLUSTER_UNALLOCATED_PLAIN = ...
|
|
168
|
+
QCOW2_SUBCLUSTER_UNALLOCATED_ALLOC = ...
|
|
169
|
+
QCOW2_SUBCLUSTER_ZERO_PLAIN = ...
|
|
170
|
+
QCOW2_SUBCLUSTER_ZERO_ALLOC = ...
|
|
171
|
+
QCOW2_SUBCLUSTER_NORMAL = ...
|
|
172
|
+
QCOW2_SUBCLUSTER_COMPRESSED = ...
|
|
173
|
+
QCOW2_SUBCLUSTER_INVALID = ...
|
|
174
|
+
|
|
175
|
+
# Technically `c_qcow2` is an instance of `_c_qcow2`, but then we can't use it in type hints
|
|
176
|
+
c_qcow2: TypeAlias = _c_qcow2
|
|
177
|
+
|
|
178
|
+
QCOW2_MAGIC: int
|
|
179
|
+
QCOW2_MAGIC_BYTES: bytes
|
|
180
|
+
|
|
181
|
+
QCOW2_INCOMPAT_MASK: int
|
|
182
|
+
|
|
183
|
+
QCow2ClusterType: TypeAlias = _c_qcow2.QCow2ClusterType
|
|
184
|
+
QCow2SubclusterType: TypeAlias = _c_qcow2.QCow2SubclusterType
|
|
185
|
+
|
|
186
|
+
NORMAL_SUBCLUSTER_TYPES: tuple[QCow2SubclusterType, ...]
|
|
187
|
+
ZERO_SUBCLUSTER_TYPES: tuple[QCow2SubclusterType, ...]
|
|
188
|
+
UNALLOCATED_SUBCLUSTER_TYPES: tuple[QCow2SubclusterType, ...]
|
|
189
|
+
|
|
190
|
+
def ctz(value: int) -> int: ...
|
dissect/hypervisor/disk/qcow2.py
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
# - https://github.com/qemu/qemu/blob/master/docs/interop/qcow2.txt
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
import copy
|
|
7
6
|
import zlib
|
|
8
7
|
from functools import cached_property, lru_cache
|
|
9
8
|
from io import BytesIO
|
|
9
|
+
from pathlib import Path
|
|
10
10
|
from typing import TYPE_CHECKING, BinaryIO
|
|
11
11
|
|
|
12
12
|
from dissect.util.stream import AlignedStream
|
|
@@ -14,6 +14,7 @@ from dissect.util.stream import AlignedStream
|
|
|
14
14
|
from dissect.hypervisor.disk.c_qcow2 import (
|
|
15
15
|
NORMAL_SUBCLUSTER_TYPES,
|
|
16
16
|
QCOW2_MAGIC,
|
|
17
|
+
QCOW2_MAGIC_BYTES,
|
|
17
18
|
UNALLOCATED_SUBCLUSTER_TYPES,
|
|
18
19
|
ZERO_SUBCLUSTER_TYPES,
|
|
19
20
|
QCow2ClusterType,
|
|
@@ -34,26 +35,42 @@ except ImportError:
|
|
|
34
35
|
HAS_ZSTD = False
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class QCow2(AlignedStream):
|
|
38
|
+
class QCow2:
|
|
41
39
|
"""QCOW2 virtual disk implementation.
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
If a data-file is required and ``fh`` is not a ``Path``, it's required to manually pass a file like object
|
|
42
|
+
in the `data_file` argument. Otherwise, the data file will be automatically opened if it exists in the same directory.
|
|
43
|
+
It's possible to defer opening the data file by passing ``allow_no_data_file=True``.
|
|
44
|
+
|
|
45
|
+
The same applies to the backing-file. This too can be deferred by passing ``allow_no_backing_file=True``.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
fh: File handle or path to the QCOW2 file.
|
|
49
|
+
data_file: Optional file handle for the data file. If not provided and ``fh`` is a ``Path``, it will try to open it automatically.
|
|
50
|
+
backing_file: Optional file handle for the backing file. If not provided and ``fh`` is a ``Path``, it will try to open it automatically.
|
|
51
|
+
allow_no_data_file: If True, allows the QCOW2 file to be opened without a data file.
|
|
52
|
+
allow_no_backing_file: If True, allows the QCOW2 file to be opened without a backing file.
|
|
53
|
+
""" # noqa: E501
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
fh: BinaryIO | Path,
|
|
58
|
+
data_file: BinaryIO | None = None,
|
|
59
|
+
backing_file: BinaryIO | None = None,
|
|
60
|
+
*,
|
|
61
|
+
allow_no_data_file: bool = False,
|
|
62
|
+
allow_no_backing_file: bool = False,
|
|
63
|
+
):
|
|
64
|
+
if isinstance(fh, Path):
|
|
65
|
+
self.path = fh
|
|
66
|
+
self.fh = self.path.open("rb")
|
|
67
|
+
else:
|
|
68
|
+
self.path = None
|
|
69
|
+
self.fh = fh
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
self.fh = fh
|
|
71
|
+
self.fh.seek(0)
|
|
55
72
|
|
|
56
|
-
self.header = c_qcow2.QCowHeader(fh)
|
|
73
|
+
self.header = c_qcow2.QCowHeader(self.fh)
|
|
57
74
|
if self.header.magic != QCOW2_MAGIC:
|
|
58
75
|
raise InvalidHeaderError("Invalid qcow2 header magic")
|
|
59
76
|
|
|
@@ -63,14 +80,16 @@ class QCow2(AlignedStream):
|
|
|
63
80
|
if self.header.cluster_bits < c_qcow2.MIN_CLUSTER_BITS or self.header.cluster_bits > c_qcow2.MAX_CLUSTER_BITS:
|
|
64
81
|
raise InvalidHeaderError(f"Unsupported cluster size: 2**{self.header.cluster_bits}")
|
|
65
82
|
|
|
83
|
+
self.size = self.header.size
|
|
84
|
+
|
|
66
85
|
self.cluster_bits = self.header.cluster_bits
|
|
67
86
|
self.cluster_size = 1 << self.cluster_bits
|
|
68
87
|
self.subclusters_per_cluster = c_qcow2.QCOW_EXTL2_SUBCLUSTERS_PER_CLUSTER if self.has_subclusters else 1
|
|
69
88
|
self.subcluster_size = self.cluster_size // self.subclusters_per_cluster
|
|
70
|
-
self.subcluster_bits = ctz(self.subcluster_size
|
|
89
|
+
self.subcluster_bits = ctz(self.subcluster_size)
|
|
71
90
|
|
|
72
91
|
self._l2_entry_size = c_qcow2.L2E_SIZE_EXTENDED if self.has_subclusters else c_qcow2.L2E_SIZE_NORMAL
|
|
73
|
-
self.l2_bits = self.cluster_bits - ctz(self._l2_entry_size
|
|
92
|
+
self.l2_bits = self.cluster_bits - ctz(self._l2_entry_size)
|
|
74
93
|
self.l2_size = 1 << self.l2_bits
|
|
75
94
|
|
|
76
95
|
# 104 = byte offset of compression_type
|
|
@@ -100,10 +119,8 @@ class QCow2(AlignedStream):
|
|
|
100
119
|
self.unknown_extensions = []
|
|
101
120
|
self._read_extensions()
|
|
102
121
|
|
|
103
|
-
if self.
|
|
104
|
-
|
|
105
|
-
raise Error(f"data-file required but not provided (image_data_file = {self.image_data_file})")
|
|
106
|
-
self.data_file = data_file
|
|
122
|
+
if self.needs_data_file:
|
|
123
|
+
self.data_file = self._open_data_file(data_file, allow_no_data_file)
|
|
107
124
|
else:
|
|
108
125
|
self.data_file = self.fh
|
|
109
126
|
|
|
@@ -115,16 +132,10 @@ class QCow2(AlignedStream):
|
|
|
115
132
|
self.auto_backing_file = self.fh.read(self.header.backing_file_size).decode()
|
|
116
133
|
self.image_backing_file = self.auto_backing_file.upper()
|
|
117
134
|
|
|
118
|
-
|
|
119
|
-
raise Error(f"backing-file required but not provided (auto_backing_file = {self.auto_backing_file})")
|
|
120
|
-
|
|
121
|
-
if backing_file != ALLOW_NO_BACKING_FILE:
|
|
122
|
-
self.backing_file = backing_file
|
|
135
|
+
self.backing_file = self._open_backing_file(backing_file, allow_no_backing_file)
|
|
123
136
|
|
|
124
137
|
self.l2_table = lru_cache(128)(self.l2_table)
|
|
125
138
|
|
|
126
|
-
super().__init__(self.header.size)
|
|
127
|
-
|
|
128
139
|
def _read_extensions(self) -> None:
|
|
129
140
|
start_offset = self.header.header_length
|
|
130
141
|
end_offset = self.header.backing_file_offset or 1 << self.cluster_bits
|
|
@@ -158,14 +169,54 @@ class QCow2(AlignedStream):
|
|
|
158
169
|
# Align to nearest 8 byte boundary
|
|
159
170
|
offset += (ext.len + 7) & 0xFFFFFFF8
|
|
160
171
|
|
|
172
|
+
def _open_data_file(self, data_file: BinaryIO | None, allow_no_data_file: bool = False) -> BinaryIO | None:
|
|
173
|
+
if data_file is not None:
|
|
174
|
+
return data_file
|
|
175
|
+
|
|
176
|
+
if self.path:
|
|
177
|
+
if (data_file_path := self.path.with_name(self.image_data_file)).exists():
|
|
178
|
+
return data_file_path.open("rb")
|
|
179
|
+
|
|
180
|
+
if not allow_no_data_file:
|
|
181
|
+
raise Error(f"data-file {str(data_file_path)!r} not found (image_data_file = {self.image_data_file!r})")
|
|
182
|
+
elif allow_no_data_file:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
raise Error(f"data-file required but not provided (image_data_file = {self.image_data_file!r})")
|
|
186
|
+
|
|
187
|
+
def _open_backing_file(self, backing_file: BinaryIO | None, allow_no_backing_file: bool = False) -> BinaryIO | None:
|
|
188
|
+
backing_file_path = None
|
|
189
|
+
if backing_file is None:
|
|
190
|
+
if self.path:
|
|
191
|
+
if (backing_file_path := self.path.with_name(self.auto_backing_file)).exists():
|
|
192
|
+
backing_file = backing_file_path.open("rb")
|
|
193
|
+
elif not allow_no_backing_file:
|
|
194
|
+
raise Error(
|
|
195
|
+
f"backing-file {str(backing_file_path)!r} not found (auto_backing_file = {self.auto_backing_file!r})" # noqa: E501
|
|
196
|
+
)
|
|
197
|
+
elif not allow_no_backing_file:
|
|
198
|
+
raise Error(f"backing-file required but not provided (auto_backing_file = {self.auto_backing_file!r})")
|
|
199
|
+
|
|
200
|
+
if backing_file:
|
|
201
|
+
if backing_file.read(4) == QCOW2_MAGIC_BYTES:
|
|
202
|
+
if backing_file_path:
|
|
203
|
+
backing_file.close()
|
|
204
|
+
backing_file = QCow2(backing_file_path).open()
|
|
205
|
+
else:
|
|
206
|
+
backing_file = QCow2(backing_file).open()
|
|
207
|
+
else:
|
|
208
|
+
backing_file.seek(0)
|
|
209
|
+
|
|
210
|
+
return backing_file
|
|
211
|
+
|
|
161
212
|
@cached_property
|
|
162
213
|
def snapshots(self) -> list[QCow2Snapshot]:
|
|
163
|
-
snapshots = []
|
|
214
|
+
snapshots: list[QCow2Snapshot] = []
|
|
164
215
|
|
|
165
216
|
offset = self.header.snapshots_offset
|
|
166
217
|
for _ in range(self.header.nb_snapshots):
|
|
167
218
|
snapshots.append(QCow2Snapshot(self, offset))
|
|
168
|
-
offset += snapshots[-1].entry_size
|
|
219
|
+
offset += (snapshots[-1].entry_size + 7) & ~7 # Round up to 8 bytes
|
|
169
220
|
|
|
170
221
|
return snapshots
|
|
171
222
|
|
|
@@ -182,65 +233,72 @@ class QCow2(AlignedStream):
|
|
|
182
233
|
def has_backing_file(self) -> bool:
|
|
183
234
|
return self.backing_file is not None
|
|
184
235
|
|
|
236
|
+
@property
|
|
237
|
+
def needs_backing_file(self) -> bool:
|
|
238
|
+
return self.header.backing_file_offset != 0
|
|
239
|
+
|
|
185
240
|
@property
|
|
186
241
|
def has_data_file(self) -> bool:
|
|
187
|
-
return self.data_file != self.fh
|
|
242
|
+
return self.data_file is not None and self.data_file != self.fh
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def needs_data_file(self) -> bool:
|
|
246
|
+
return bool(self.header.incompatible_features & c_qcow2.QCOW2_INCOMPAT_DATA_FILE)
|
|
188
247
|
|
|
189
248
|
@property
|
|
190
249
|
def has_subclusters(self) -> bool:
|
|
191
250
|
return bool(self.header.incompatible_features & c_qcow2.QCOW2_INCOMPAT_EXTL2)
|
|
192
251
|
|
|
193
|
-
def
|
|
194
|
-
|
|
252
|
+
def open(self) -> QCow2Stream:
|
|
253
|
+
"""Open the QCow2 file for reading."""
|
|
254
|
+
if self.needs_data_file and not self.has_data_file:
|
|
255
|
+
raise Error(f"data-file required but not provided (image_data_file = {self.image_data_file!r})")
|
|
256
|
+
if self.needs_backing_file and not self.has_backing_file:
|
|
257
|
+
raise Error(f"backing-file required but not provided (auto_backing_file = {self.auto_backing_file!r})")
|
|
258
|
+
return QCow2Stream(self)
|
|
195
259
|
|
|
196
|
-
for sc_type, read_offset, run_offset, run_length in self._yield_runs(offset, length):
|
|
197
|
-
unalloc_zeroed = sc_type in UNALLOCATED_SUBCLUSTER_TYPES and not self.has_backing_file
|
|
198
260
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
elif sc_type in UNALLOCATED_SUBCLUSTER_TYPES and self.has_backing_file:
|
|
202
|
-
self.backing_file.seek(read_offset)
|
|
203
|
-
result.append(self.backing_file.read(run_length))
|
|
204
|
-
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED:
|
|
205
|
-
result.append(self._read_compressed(run_offset, read_offset, run_length))
|
|
206
|
-
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL:
|
|
207
|
-
self.data_file.seek(run_offset)
|
|
208
|
-
result.append(self.data_file.read(run_length))
|
|
261
|
+
class QCow2Snapshot:
|
|
262
|
+
"""Wrapper class for snapshot table entries."""
|
|
209
263
|
|
|
210
|
-
|
|
264
|
+
def __init__(self, qcow2: QCow2, offset: int):
|
|
265
|
+
self.qcow2 = qcow2
|
|
266
|
+
self.offset = offset
|
|
211
267
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
coffset = cluster_descriptor & self.cluster_offset_mask
|
|
215
|
-
nb_csectors = ((cluster_descriptor >> self.csize_shift) & self.csize_mask) + 1
|
|
216
|
-
# Original source uses the mask ~(~(QCOW2_COMPRESSED_SECTOR_SIZE - 1ULL))
|
|
217
|
-
# However bit inversion is weird in Python, and this evaluates to 511, so we use that value instead.
|
|
218
|
-
csize = nb_csectors * c_qcow2.QCOW2_COMPRESSED_SECTOR_SIZE - (coffset & 511)
|
|
268
|
+
self.qcow2.fh.seek(offset)
|
|
269
|
+
self.header = c_qcow2.QCowSnapshotHeader(self.qcow2.fh)
|
|
219
270
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
271
|
+
# Older versions may not have all the extra data fields
|
|
272
|
+
# Instead of reading them manually, just pad the extra data to fit our struct
|
|
273
|
+
extra_data = self.qcow2.fh.read(self.header.extra_data_size)
|
|
274
|
+
self.extra = c_qcow2.QCowSnapshotExtraData(extra_data.ljust(len(c_qcow2.QCowSnapshotExtraData), b"\x00"))
|
|
223
275
|
|
|
224
|
-
|
|
276
|
+
unknown_extra_size = self.header.extra_data_size - len(c_qcow2.QCowSnapshotExtraData)
|
|
277
|
+
self.unknown_extra = self.qcow2.fh.read(unknown_extra_size) if unknown_extra_size > 0 else None
|
|
225
278
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
dctx = zlib.decompressobj(-12)
|
|
229
|
-
return dctx.decompress(buf, self.cluster_size)
|
|
279
|
+
self.id = self.qcow2.fh.read(self.header.id_str_size).decode()
|
|
280
|
+
self.name = self.qcow2.fh.read(self.header.name_size).decode()
|
|
230
281
|
|
|
231
|
-
|
|
232
|
-
result = []
|
|
282
|
+
self.entry_size = self.qcow2.fh.tell() - offset
|
|
233
283
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
284
|
+
def open(self) -> QCow2Stream:
|
|
285
|
+
"""Open the snapshot for reading."""
|
|
286
|
+
return QCow2Stream(self.qcow2, self.l1_table)
|
|
287
|
+
|
|
288
|
+
@cached_property
|
|
289
|
+
def l1_table(self) -> list[int]:
|
|
290
|
+
# L1 table is usually relatively small, it can be at most 32MB on PB or EB size disks
|
|
291
|
+
self.qcow2.fh.seek(self.header.l1_table_offset)
|
|
292
|
+
return c_qcow2.uint64[self.header.l1_size](self.qcow2.fh)
|
|
242
293
|
|
|
243
|
-
|
|
294
|
+
|
|
295
|
+
class QCow2Stream(AlignedStream):
|
|
296
|
+
"""Aligned stream for reading QCow2 files."""
|
|
297
|
+
|
|
298
|
+
def __init__(self, qcow2: QCow2, l1_table: list[int] | None = None):
|
|
299
|
+
super().__init__(qcow2.header.size, align=qcow2.cluster_size)
|
|
300
|
+
self.qcow2 = qcow2
|
|
301
|
+
self.l1_table = l1_table or qcow2.l1_table
|
|
244
302
|
|
|
245
303
|
def _yield_runs(self, offset: int, length: int) -> Iterator[tuple[QCow2SubclusterType, int, int, int]]:
|
|
246
304
|
# reference: qcow2_get_host_offset
|
|
@@ -249,19 +307,19 @@ class QCow2(AlignedStream):
|
|
|
249
307
|
host_offset = 0
|
|
250
308
|
read_count = 0
|
|
251
309
|
|
|
252
|
-
l1_index = offset_to_l1_index(self, offset)
|
|
253
|
-
l2_index = offset_to_l2_index(self, offset)
|
|
254
|
-
sc_index = offset_to_sc_index(self, offset)
|
|
310
|
+
l1_index = offset_to_l1_index(self.qcow2, offset)
|
|
311
|
+
l2_index = offset_to_l2_index(self.qcow2, offset)
|
|
312
|
+
sc_index = offset_to_sc_index(self.qcow2, offset)
|
|
255
313
|
|
|
256
|
-
offset_in_cluster = offset_into_cluster(self, offset)
|
|
314
|
+
offset_in_cluster = offset_into_cluster(self.qcow2, offset)
|
|
257
315
|
|
|
258
316
|
bytes_needed = length + offset_in_cluster
|
|
259
317
|
# at the time being we just use the entire l2 table and not cached slices
|
|
260
318
|
# this is actually the bytes available/remaining in this l2 table
|
|
261
|
-
bytes_available = (self.l2_size - l2_index) << self.cluster_bits
|
|
319
|
+
bytes_available = (self.qcow2.l2_size - l2_index) << self.qcow2.cluster_bits
|
|
262
320
|
bytes_needed = min(bytes_needed, bytes_available)
|
|
263
321
|
|
|
264
|
-
if l1_index > self.header.l1_size:
|
|
322
|
+
if l1_index > self.qcow2.header.l1_size:
|
|
265
323
|
# bytes_needed is already the smaller value here
|
|
266
324
|
read_count = bytes_needed - offset_in_cluster
|
|
267
325
|
|
|
@@ -282,11 +340,11 @@ class QCow2(AlignedStream):
|
|
|
282
340
|
offset += read_count
|
|
283
341
|
continue
|
|
284
342
|
|
|
285
|
-
l2_table = self.l2_table(l2_offset)
|
|
343
|
+
l2_table = self.qcow2.l2_table(l2_offset)
|
|
286
344
|
l2_entry = l2_table.entry(l2_index)
|
|
287
345
|
l2_bitmap = l2_table.bitmap(l2_index)
|
|
288
346
|
|
|
289
|
-
sc_type = get_subcluster_type(self, l2_entry, l2_bitmap, sc_index)
|
|
347
|
+
sc_type = get_subcluster_type(self.qcow2, l2_entry, l2_bitmap, sc_index)
|
|
290
348
|
|
|
291
349
|
if sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED:
|
|
292
350
|
host_offset = l2_entry & c_qcow2.L2E_COMPRESSED_OFFSET_SIZE_MASK
|
|
@@ -294,10 +352,10 @@ class QCow2(AlignedStream):
|
|
|
294
352
|
host_cluster_offset = l2_entry & c_qcow2.L2E_OFFSET_MASK
|
|
295
353
|
host_offset = host_cluster_offset + offset_in_cluster
|
|
296
354
|
|
|
297
|
-
nb_clusters = size_to_clusters(self, bytes_needed)
|
|
298
|
-
sc_count = count_contiguous_subclusters(self, nb_clusters, sc_index, l2_table, l2_index)
|
|
355
|
+
nb_clusters = size_to_clusters(self.qcow2, bytes_needed)
|
|
356
|
+
sc_count = count_contiguous_subclusters(self.qcow2, nb_clusters, sc_index, l2_table, l2_index)
|
|
299
357
|
# this is the amount of contiguous bytes available of the same subcluster type
|
|
300
|
-
bytes_available = (sc_count + sc_index) << self.subcluster_bits
|
|
358
|
+
bytes_available = (sc_count + sc_index) << self.qcow2.subcluster_bits
|
|
301
359
|
|
|
302
360
|
# account for the offset in the cluster
|
|
303
361
|
read_count = min(bytes_available, bytes_needed) - offset_in_cluster
|
|
@@ -306,6 +364,58 @@ class QCow2(AlignedStream):
|
|
|
306
364
|
length -= read_count
|
|
307
365
|
offset += read_count
|
|
308
366
|
|
|
367
|
+
def _read_compressed(self, cluster_descriptor: int, offset: int, length: int) -> bytes:
|
|
368
|
+
offset_in_cluster = offset_into_cluster(self.qcow2, offset)
|
|
369
|
+
coffset = cluster_descriptor & self.qcow2.cluster_offset_mask
|
|
370
|
+
nb_csectors = ((cluster_descriptor >> self.qcow2.csize_shift) & self.qcow2.csize_mask) + 1
|
|
371
|
+
# Original source uses the mask ~(~(QCOW2_COMPRESSED_SECTOR_SIZE - 1ULL))
|
|
372
|
+
# However bit inversion is weird in Python, and this evaluates to 511, so we use that value instead.
|
|
373
|
+
csize = nb_csectors * c_qcow2.QCOW2_COMPRESSED_SECTOR_SIZE - (coffset & 511)
|
|
374
|
+
|
|
375
|
+
self.qcow2.fh.seek(coffset)
|
|
376
|
+
buf = self.qcow2.fh.read(csize)
|
|
377
|
+
decompressed = self._decompress(buf)
|
|
378
|
+
|
|
379
|
+
return decompressed[offset_in_cluster : offset_in_cluster + length]
|
|
380
|
+
|
|
381
|
+
def _decompress(self, buf: bytes) -> bytes:
|
|
382
|
+
if self.qcow2.compression_type == c_qcow2.QCOW2_COMPRESSION_TYPE_ZLIB:
|
|
383
|
+
dctx = zlib.decompressobj(-12)
|
|
384
|
+
return dctx.decompress(buf, self.qcow2.cluster_size)
|
|
385
|
+
|
|
386
|
+
if self.qcow2.compression_type == c_qcow2.QCOW2_COMPRESSION_TYPE_ZSTD:
|
|
387
|
+
result = []
|
|
388
|
+
|
|
389
|
+
dctx = zstd.ZstdDecompressor()
|
|
390
|
+
reader = dctx.stream_reader(BytesIO(buf))
|
|
391
|
+
while reader.tell() < self.qcow2.cluster_size:
|
|
392
|
+
chunk = reader.read(self.qcow2.cluster_size - reader.tell())
|
|
393
|
+
if not chunk:
|
|
394
|
+
break
|
|
395
|
+
result.append(chunk)
|
|
396
|
+
return b"".join(result)
|
|
397
|
+
|
|
398
|
+
raise Error(f"Invalid compression type: {self.qcow2.compression_type}")
|
|
399
|
+
|
|
400
|
+
def _read(self, offset: int, length: int) -> bytes:
|
|
401
|
+
result = []
|
|
402
|
+
|
|
403
|
+
for sc_type, read_offset, run_offset, run_length in self._yield_runs(offset, length):
|
|
404
|
+
unalloc_zeroed = sc_type in UNALLOCATED_SUBCLUSTER_TYPES and not self.qcow2.has_backing_file
|
|
405
|
+
|
|
406
|
+
if sc_type in ZERO_SUBCLUSTER_TYPES or unalloc_zeroed:
|
|
407
|
+
result.append(b"\x00" * run_length)
|
|
408
|
+
elif sc_type in UNALLOCATED_SUBCLUSTER_TYPES and self.qcow2.has_backing_file:
|
|
409
|
+
self.qcow2.backing_file.seek(read_offset)
|
|
410
|
+
result.append(self.qcow2.backing_file.read(run_length))
|
|
411
|
+
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED:
|
|
412
|
+
result.append(self._read_compressed(run_offset, read_offset, run_length))
|
|
413
|
+
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL:
|
|
414
|
+
self.qcow2.data_file.seek(run_offset)
|
|
415
|
+
result.append(self.qcow2.data_file.read(run_length))
|
|
416
|
+
|
|
417
|
+
return b"".join(result)
|
|
418
|
+
|
|
309
419
|
|
|
310
420
|
class L2Table:
|
|
311
421
|
"""Convenience class for accessing the L2 table."""
|
|
@@ -327,42 +437,6 @@ class L2Table:
|
|
|
327
437
|
return 0
|
|
328
438
|
|
|
329
439
|
|
|
330
|
-
class QCow2Snapshot:
|
|
331
|
-
"""Wrapper class for snapshot table entries."""
|
|
332
|
-
|
|
333
|
-
def __init__(self, qcow2: QCow2, offset: int):
|
|
334
|
-
self.qcow2 = qcow2
|
|
335
|
-
self.offset = offset
|
|
336
|
-
|
|
337
|
-
self.qcow2.fh.seek(offset)
|
|
338
|
-
self.header = c_qcow2.QCowSnapshotHeader(self.qcow2.fh)
|
|
339
|
-
|
|
340
|
-
# Older versions may not have all the extra data fields
|
|
341
|
-
# Instead of reading them manually, just pad the extra data to fit our struct
|
|
342
|
-
extra_data = self.qcow2.fh.read(self.header.extra_data_size)
|
|
343
|
-
self.extra = c_qcow2.QCowSnapshotExtraData(extra_data.ljust(len(c_qcow2.QCowSnapshotExtraData), b"\x00"))
|
|
344
|
-
|
|
345
|
-
unknown_extra_size = self.header.extra_data_size - len(c_qcow2.QCowSnapshotExtraData)
|
|
346
|
-
self.unknown_extra = self.qcow2.fh.read(unknown_extra_size) if unknown_extra_size > 0 else None
|
|
347
|
-
|
|
348
|
-
self.id_str = self.qcow2.fh.read(self.header.id_str_size).decode()
|
|
349
|
-
self.name = self.qcow2.fh.read(self.header.name_size).decode()
|
|
350
|
-
|
|
351
|
-
self.entry_size = self.qcow2.fh.tell() - offset
|
|
352
|
-
|
|
353
|
-
def open(self) -> QCow2:
|
|
354
|
-
disk = copy.copy(self.qcow2)
|
|
355
|
-
disk.l1_table = self.l1_table
|
|
356
|
-
disk.seek(0)
|
|
357
|
-
return disk
|
|
358
|
-
|
|
359
|
-
@cached_property
|
|
360
|
-
def l1_table(self) -> list[int]:
|
|
361
|
-
# L1 table is usually relatively small, it can be at most 32MB on PB or EB size disks
|
|
362
|
-
self.qcow2.fh.seek(self.header.l1_table_offset)
|
|
363
|
-
return c_qcow2.uint64[self.header.l1_size](self.qcow2.fh)
|
|
364
|
-
|
|
365
|
-
|
|
366
440
|
def offset_into_cluster(qcow2: QCow2, offset: int) -> int:
|
|
367
441
|
return offset & (qcow2.cluster_size - 1)
|
|
368
442
|
|
|
@@ -460,16 +534,16 @@ def get_subcluster_range_type(
|
|
|
460
534
|
sc_mask = (1 << sc_from) - 1
|
|
461
535
|
if sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL:
|
|
462
536
|
val = l2_bitmap | sc_mask # QCOW_OFLAG_SUB_ALLOC_RANGE(0, sc_from)
|
|
463
|
-
return sc_type, ctz(val
|
|
537
|
+
return sc_type, ctz(val) - sc_from
|
|
464
538
|
if sc_type in ZERO_SUBCLUSTER_TYPES:
|
|
465
539
|
val = (l2_bitmap | sc_mask) >> 32 # QCOW_OFLAG_SUB_ZERO_RANGE(0, sc_from)
|
|
466
|
-
return sc_type, ctz(val
|
|
540
|
+
return sc_type, ctz(val) - sc_from
|
|
467
541
|
if sc_type in UNALLOCATED_SUBCLUSTER_TYPES:
|
|
468
542
|
# We need to mask it with a 64bit mask because Python flips the sign bit
|
|
469
543
|
inv_mask = ~sc_mask & ((1 << 64) - 1) # ~QCOW_OFLAG_SUB_ALLOC_RANGE(0, sc_from)
|
|
470
544
|
|
|
471
545
|
val = ((l2_bitmap >> 32) | l2_bitmap) & inv_mask
|
|
472
|
-
return sc_type, ctz(val
|
|
546
|
+
return sc_type, ctz(val) - sc_from
|
|
473
547
|
|
|
474
548
|
raise Error(f"Invalid subcluster type: {sc_type}")
|
|
475
549
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.hypervisor
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.19.dev2
|
|
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
|
|
@@ -9,13 +9,14 @@ dissect/hypervisor/descriptor/vbox.py,sha256=TrfMN0BppjIoDeHVCMKlH1OS0IHO-V7uRFo
|
|
|
9
9
|
dissect/hypervisor/descriptor/vmx.py,sha256=6xhX7RlDqyn9XBkiq2NnQ5LrLL0nXwiPGOPy3WVVAtI,12647
|
|
10
10
|
dissect/hypervisor/disk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
dissect/hypervisor/disk/c_hdd.py,sha256=wrJ5qwyf7QCZ3Pl-I8eYDR4CUUo4pAFhjzs8huRGqs8,2235
|
|
12
|
-
dissect/hypervisor/disk/c_qcow2.py,sha256=
|
|
12
|
+
dissect/hypervisor/disk/c_qcow2.py,sha256=jOr2fQmDQuegbE1qDX2x2xaA4RS0LKMsOCI5UwKJxhg,5071
|
|
13
|
+
dissect/hypervisor/disk/c_qcow2.pyi,sha256=Rf-tCTf78F2fsVBYw04cZ2vNq2Xns7P1f86oSVe7zEU,7896
|
|
13
14
|
dissect/hypervisor/disk/c_vdi.py,sha256=lKk5MnCImdfyUyGDe_yY_ntLd1ITkKEvMGIMCHiHC2w,3843
|
|
14
15
|
dissect/hypervisor/disk/c_vhd.py,sha256=gYOwOdvhMKNLflugkpjmdn__ZUJBlxeByx6P0ePNQ00,1493
|
|
15
16
|
dissect/hypervisor/disk/c_vhdx.py,sha256=4yNU51jfYPEBFXEZAvaF8cFdHBRlmCe_OU5ZU83-kco,2920
|
|
16
17
|
dissect/hypervisor/disk/c_vmdk.py,sha256=lfObMyD9r-20DCDwVDm9xT98cq3aE4MCJF5TpDBNdd4,5988
|
|
17
18
|
dissect/hypervisor/disk/hdd.py,sha256=ChKtQIQrssIgZyUv86JE9-n4XdjDT14pc6EO6mZllQg,12981
|
|
18
|
-
dissect/hypervisor/disk/qcow2.py,sha256=
|
|
19
|
+
dissect/hypervisor/disk/qcow2.py,sha256=j7GSgzYceSq6Nx7vw4M7Cm5irKqGDql1t9apl2gC1FY,24509
|
|
19
20
|
dissect/hypervisor/disk/vdi.py,sha256=Ezm_vmi6ZKLTgPx5chXmtBzkBOCxilBWUrdqQdq_4KA,1958
|
|
20
21
|
dissect/hypervisor/disk/vhd.py,sha256=GfEfFp5g8OlV0MrTXqSfBRzMMvIZO_T1A8d05SeGiBk,3948
|
|
21
22
|
dissect/hypervisor/disk/vhdx.py,sha256=Yidz57PsH6IGRiN8cpfAnyZiOGt1qApd8ohVdHGF2mY,13215
|
|
@@ -26,10 +27,10 @@ dissect/hypervisor/tools/vmtar.py,sha256=bAf_rEhqUmeI8my22p7L9uV3SgB5C_61YQKfD3t
|
|
|
26
27
|
dissect/hypervisor/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
28
|
dissect/hypervisor/util/envelope.py,sha256=EhZjZh3EezqxkJ84OgqpptHIhpVWPcUkFMjdYU-iAnU,10442
|
|
28
29
|
dissect/hypervisor/util/vmtar.py,sha256=xzc4bd5RnBgP9_dCU9i-ZwXI_0S3N7gCfMzElwwnd5s,4004
|
|
29
|
-
dissect_hypervisor-3.
|
|
30
|
-
dissect_hypervisor-3.
|
|
31
|
-
dissect_hypervisor-3.
|
|
32
|
-
dissect_hypervisor-3.
|
|
33
|
-
dissect_hypervisor-3.
|
|
34
|
-
dissect_hypervisor-3.
|
|
35
|
-
dissect_hypervisor-3.
|
|
30
|
+
dissect_hypervisor-3.19.dev2.dist-info/licenses/COPYRIGHT,sha256=EOOoIwk_inOMUD4c1ylpzMtYLjGzmc-MLEVAEdLLr20,305
|
|
31
|
+
dissect_hypervisor-3.19.dev2.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
32
|
+
dissect_hypervisor-3.19.dev2.dist-info/METADATA,sha256=58CgYl9rnPBcQauWn5zLrrdF6jR6N6oI1eYENq8W3ek,3434
|
|
33
|
+
dissect_hypervisor-3.19.dev2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
34
|
+
dissect_hypervisor-3.19.dev2.dist-info/entry_points.txt,sha256=kW36GnJ3G1dSRYFL4r8Bj32Al_CkGqST3bdHvo3pyiY,120
|
|
35
|
+
dissect_hypervisor-3.19.dev2.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
|
36
|
+
dissect_hypervisor-3.19.dev2.dist-info/RECORD,,
|
{dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/COPYRIGHT
RENAMED
|
File without changes
|
{dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{dissect_hypervisor-3.18.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/top_level.txt
RENAMED
|
File without changes
|