dissect.hypervisor 3.19.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 +193 -130
- {dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/METADATA +1 -1
- {dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/RECORD +10 -9
- {dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/WHEEL +0 -0
- {dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/entry_points.txt +0 -0
- {dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/COPYRIGHT +0 -0
- {dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/LICENSE +0 -0
- {dissect_hypervisor-3.19.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,7 +3,6 @@
|
|
|
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
|
|
@@ -15,6 +14,7 @@ from dissect.util.stream import AlignedStream
|
|
|
15
14
|
from dissect.hypervisor.disk.c_qcow2 import (
|
|
16
15
|
NORMAL_SUBCLUSTER_TYPES,
|
|
17
16
|
QCOW2_MAGIC,
|
|
17
|
+
QCOW2_MAGIC_BYTES,
|
|
18
18
|
UNALLOCATED_SUBCLUSTER_TYPES,
|
|
19
19
|
ZERO_SUBCLUSTER_TYPES,
|
|
20
20
|
QCow2ClusterType,
|
|
@@ -35,28 +35,42 @@ except ImportError:
|
|
|
35
35
|
HAS_ZSTD = False
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class QCow2(AlignedStream):
|
|
38
|
+
class QCow2:
|
|
42
39
|
"""QCOW2 virtual disk implementation.
|
|
43
40
|
|
|
44
|
-
|
|
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``.
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
as the `data_file` argument.
|
|
45
|
+
The same applies to the backing-file. This too can be deferred by passing ``allow_no_backing_file=True``.
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
53
54
|
|
|
54
55
|
def __init__(
|
|
55
|
-
self,
|
|
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,
|
|
56
63
|
):
|
|
57
|
-
|
|
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
|
|
58
70
|
|
|
59
|
-
self.
|
|
71
|
+
self.fh.seek(0)
|
|
72
|
+
|
|
73
|
+
self.header = c_qcow2.QCowHeader(self.fh)
|
|
60
74
|
if self.header.magic != QCOW2_MAGIC:
|
|
61
75
|
raise InvalidHeaderError("Invalid qcow2 header magic")
|
|
62
76
|
|
|
@@ -66,14 +80,16 @@ class QCow2(AlignedStream):
|
|
|
66
80
|
if self.header.cluster_bits < c_qcow2.MIN_CLUSTER_BITS or self.header.cluster_bits > c_qcow2.MAX_CLUSTER_BITS:
|
|
67
81
|
raise InvalidHeaderError(f"Unsupported cluster size: 2**{self.header.cluster_bits}")
|
|
68
82
|
|
|
83
|
+
self.size = self.header.size
|
|
84
|
+
|
|
69
85
|
self.cluster_bits = self.header.cluster_bits
|
|
70
86
|
self.cluster_size = 1 << self.cluster_bits
|
|
71
87
|
self.subclusters_per_cluster = c_qcow2.QCOW_EXTL2_SUBCLUSTERS_PER_CLUSTER if self.has_subclusters else 1
|
|
72
88
|
self.subcluster_size = self.cluster_size // self.subclusters_per_cluster
|
|
73
|
-
self.subcluster_bits = ctz(self.subcluster_size
|
|
89
|
+
self.subcluster_bits = ctz(self.subcluster_size)
|
|
74
90
|
|
|
75
91
|
self._l2_entry_size = c_qcow2.L2E_SIZE_EXTENDED if self.has_subclusters else c_qcow2.L2E_SIZE_NORMAL
|
|
76
|
-
self.l2_bits = self.cluster_bits - ctz(self._l2_entry_size
|
|
92
|
+
self.l2_bits = self.cluster_bits - ctz(self._l2_entry_size)
|
|
77
93
|
self.l2_size = 1 << self.l2_bits
|
|
78
94
|
|
|
79
95
|
# 104 = byte offset of compression_type
|
|
@@ -103,10 +119,8 @@ class QCow2(AlignedStream):
|
|
|
103
119
|
self.unknown_extensions = []
|
|
104
120
|
self._read_extensions()
|
|
105
121
|
|
|
106
|
-
if self.
|
|
107
|
-
|
|
108
|
-
raise Error(f"data-file required but not provided (image_data_file = {self.image_data_file})")
|
|
109
|
-
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)
|
|
110
124
|
else:
|
|
111
125
|
self.data_file = self.fh
|
|
112
126
|
|
|
@@ -118,24 +132,10 @@ class QCow2(AlignedStream):
|
|
|
118
132
|
self.auto_backing_file = self.fh.read(self.header.backing_file_size).decode()
|
|
119
133
|
self.image_backing_file = self.auto_backing_file.upper()
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
if not isinstance(fh, Path):
|
|
123
|
-
raise Error(
|
|
124
|
-
f"backing-file required but not provided (auto_backing_file = {self.auto_backing_file})"
|
|
125
|
-
)
|
|
126
|
-
if not (candidate_path := fh.parent.joinpath(self.auto_backing_file)).exists():
|
|
127
|
-
raise Error(
|
|
128
|
-
f"backing-file '{candidate_path}' not found (auto_backing_file = '{self.auto_backing_file}')"
|
|
129
|
-
)
|
|
130
|
-
backing_file = candidate_path.open("rb")
|
|
131
|
-
|
|
132
|
-
if backing_file != ALLOW_NO_BACKING_FILE:
|
|
133
|
-
self.backing_file = backing_file
|
|
135
|
+
self.backing_file = self._open_backing_file(backing_file, allow_no_backing_file)
|
|
134
136
|
|
|
135
137
|
self.l2_table = lru_cache(128)(self.l2_table)
|
|
136
138
|
|
|
137
|
-
super().__init__(self.header.size)
|
|
138
|
-
|
|
139
139
|
def _read_extensions(self) -> None:
|
|
140
140
|
start_offset = self.header.header_length
|
|
141
141
|
end_offset = self.header.backing_file_offset or 1 << self.cluster_bits
|
|
@@ -169,14 +169,54 @@ class QCow2(AlignedStream):
|
|
|
169
169
|
# Align to nearest 8 byte boundary
|
|
170
170
|
offset += (ext.len + 7) & 0xFFFFFFF8
|
|
171
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
|
+
|
|
172
212
|
@cached_property
|
|
173
213
|
def snapshots(self) -> list[QCow2Snapshot]:
|
|
174
|
-
snapshots = []
|
|
214
|
+
snapshots: list[QCow2Snapshot] = []
|
|
175
215
|
|
|
176
216
|
offset = self.header.snapshots_offset
|
|
177
217
|
for _ in range(self.header.nb_snapshots):
|
|
178
218
|
snapshots.append(QCow2Snapshot(self, offset))
|
|
179
|
-
offset += snapshots[-1].entry_size
|
|
219
|
+
offset += (snapshots[-1].entry_size + 7) & ~7 # Round up to 8 bytes
|
|
180
220
|
|
|
181
221
|
return snapshots
|
|
182
222
|
|
|
@@ -193,65 +233,72 @@ class QCow2(AlignedStream):
|
|
|
193
233
|
def has_backing_file(self) -> bool:
|
|
194
234
|
return self.backing_file is not None
|
|
195
235
|
|
|
236
|
+
@property
|
|
237
|
+
def needs_backing_file(self) -> bool:
|
|
238
|
+
return self.header.backing_file_offset != 0
|
|
239
|
+
|
|
196
240
|
@property
|
|
197
241
|
def has_data_file(self) -> bool:
|
|
198
|
-
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)
|
|
199
247
|
|
|
200
248
|
@property
|
|
201
249
|
def has_subclusters(self) -> bool:
|
|
202
250
|
return bool(self.header.incompatible_features & c_qcow2.QCOW2_INCOMPAT_EXTL2)
|
|
203
251
|
|
|
204
|
-
def
|
|
205
|
-
|
|
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)
|
|
206
259
|
|
|
207
|
-
for sc_type, read_offset, run_offset, run_length in self._yield_runs(offset, length):
|
|
208
|
-
unalloc_zeroed = sc_type in UNALLOCATED_SUBCLUSTER_TYPES and not self.has_backing_file
|
|
209
260
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
elif sc_type in UNALLOCATED_SUBCLUSTER_TYPES and self.has_backing_file:
|
|
213
|
-
self.backing_file.seek(read_offset)
|
|
214
|
-
result.append(self.backing_file.read(run_length))
|
|
215
|
-
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED:
|
|
216
|
-
result.append(self._read_compressed(run_offset, read_offset, run_length))
|
|
217
|
-
elif sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL:
|
|
218
|
-
self.data_file.seek(run_offset)
|
|
219
|
-
result.append(self.data_file.read(run_length))
|
|
261
|
+
class QCow2Snapshot:
|
|
262
|
+
"""Wrapper class for snapshot table entries."""
|
|
220
263
|
|
|
221
|
-
|
|
264
|
+
def __init__(self, qcow2: QCow2, offset: int):
|
|
265
|
+
self.qcow2 = qcow2
|
|
266
|
+
self.offset = offset
|
|
222
267
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
coffset = cluster_descriptor & self.cluster_offset_mask
|
|
226
|
-
nb_csectors = ((cluster_descriptor >> self.csize_shift) & self.csize_mask) + 1
|
|
227
|
-
# Original source uses the mask ~(~(QCOW2_COMPRESSED_SECTOR_SIZE - 1ULL))
|
|
228
|
-
# However bit inversion is weird in Python, and this evaluates to 511, so we use that value instead.
|
|
229
|
-
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)
|
|
230
270
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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"))
|
|
234
275
|
|
|
235
|
-
|
|
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
|
|
236
278
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
dctx = zlib.decompressobj(-12)
|
|
240
|
-
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()
|
|
241
281
|
|
|
242
|
-
|
|
243
|
-
result = []
|
|
282
|
+
self.entry_size = self.qcow2.fh.tell() - offset
|
|
244
283
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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)
|
|
253
293
|
|
|
254
|
-
|
|
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
|
|
255
302
|
|
|
256
303
|
def _yield_runs(self, offset: int, length: int) -> Iterator[tuple[QCow2SubclusterType, int, int, int]]:
|
|
257
304
|
# reference: qcow2_get_host_offset
|
|
@@ -260,19 +307,19 @@ class QCow2(AlignedStream):
|
|
|
260
307
|
host_offset = 0
|
|
261
308
|
read_count = 0
|
|
262
309
|
|
|
263
|
-
l1_index = offset_to_l1_index(self, offset)
|
|
264
|
-
l2_index = offset_to_l2_index(self, offset)
|
|
265
|
-
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)
|
|
266
313
|
|
|
267
|
-
offset_in_cluster = offset_into_cluster(self, offset)
|
|
314
|
+
offset_in_cluster = offset_into_cluster(self.qcow2, offset)
|
|
268
315
|
|
|
269
316
|
bytes_needed = length + offset_in_cluster
|
|
270
317
|
# at the time being we just use the entire l2 table and not cached slices
|
|
271
318
|
# this is actually the bytes available/remaining in this l2 table
|
|
272
|
-
bytes_available = (self.l2_size - l2_index) << self.cluster_bits
|
|
319
|
+
bytes_available = (self.qcow2.l2_size - l2_index) << self.qcow2.cluster_bits
|
|
273
320
|
bytes_needed = min(bytes_needed, bytes_available)
|
|
274
321
|
|
|
275
|
-
if l1_index > self.header.l1_size:
|
|
322
|
+
if l1_index > self.qcow2.header.l1_size:
|
|
276
323
|
# bytes_needed is already the smaller value here
|
|
277
324
|
read_count = bytes_needed - offset_in_cluster
|
|
278
325
|
|
|
@@ -293,11 +340,11 @@ class QCow2(AlignedStream):
|
|
|
293
340
|
offset += read_count
|
|
294
341
|
continue
|
|
295
342
|
|
|
296
|
-
l2_table = self.l2_table(l2_offset)
|
|
343
|
+
l2_table = self.qcow2.l2_table(l2_offset)
|
|
297
344
|
l2_entry = l2_table.entry(l2_index)
|
|
298
345
|
l2_bitmap = l2_table.bitmap(l2_index)
|
|
299
346
|
|
|
300
|
-
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)
|
|
301
348
|
|
|
302
349
|
if sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_COMPRESSED:
|
|
303
350
|
host_offset = l2_entry & c_qcow2.L2E_COMPRESSED_OFFSET_SIZE_MASK
|
|
@@ -305,10 +352,10 @@ class QCow2(AlignedStream):
|
|
|
305
352
|
host_cluster_offset = l2_entry & c_qcow2.L2E_OFFSET_MASK
|
|
306
353
|
host_offset = host_cluster_offset + offset_in_cluster
|
|
307
354
|
|
|
308
|
-
nb_clusters = size_to_clusters(self, bytes_needed)
|
|
309
|
-
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)
|
|
310
357
|
# this is the amount of contiguous bytes available of the same subcluster type
|
|
311
|
-
bytes_available = (sc_count + sc_index) << self.subcluster_bits
|
|
358
|
+
bytes_available = (sc_count + sc_index) << self.qcow2.subcluster_bits
|
|
312
359
|
|
|
313
360
|
# account for the offset in the cluster
|
|
314
361
|
read_count = min(bytes_available, bytes_needed) - offset_in_cluster
|
|
@@ -317,6 +364,58 @@ class QCow2(AlignedStream):
|
|
|
317
364
|
length -= read_count
|
|
318
365
|
offset += read_count
|
|
319
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
|
+
|
|
320
419
|
|
|
321
420
|
class L2Table:
|
|
322
421
|
"""Convenience class for accessing the L2 table."""
|
|
@@ -338,42 +437,6 @@ class L2Table:
|
|
|
338
437
|
return 0
|
|
339
438
|
|
|
340
439
|
|
|
341
|
-
class QCow2Snapshot:
|
|
342
|
-
"""Wrapper class for snapshot table entries."""
|
|
343
|
-
|
|
344
|
-
def __init__(self, qcow2: QCow2, offset: int):
|
|
345
|
-
self.qcow2 = qcow2
|
|
346
|
-
self.offset = offset
|
|
347
|
-
|
|
348
|
-
self.qcow2.fh.seek(offset)
|
|
349
|
-
self.header = c_qcow2.QCowSnapshotHeader(self.qcow2.fh)
|
|
350
|
-
|
|
351
|
-
# Older versions may not have all the extra data fields
|
|
352
|
-
# Instead of reading them manually, just pad the extra data to fit our struct
|
|
353
|
-
extra_data = self.qcow2.fh.read(self.header.extra_data_size)
|
|
354
|
-
self.extra = c_qcow2.QCowSnapshotExtraData(extra_data.ljust(len(c_qcow2.QCowSnapshotExtraData), b"\x00"))
|
|
355
|
-
|
|
356
|
-
unknown_extra_size = self.header.extra_data_size - len(c_qcow2.QCowSnapshotExtraData)
|
|
357
|
-
self.unknown_extra = self.qcow2.fh.read(unknown_extra_size) if unknown_extra_size > 0 else None
|
|
358
|
-
|
|
359
|
-
self.id_str = self.qcow2.fh.read(self.header.id_str_size).decode()
|
|
360
|
-
self.name = self.qcow2.fh.read(self.header.name_size).decode()
|
|
361
|
-
|
|
362
|
-
self.entry_size = self.qcow2.fh.tell() - offset
|
|
363
|
-
|
|
364
|
-
def open(self) -> QCow2:
|
|
365
|
-
disk = copy.copy(self.qcow2)
|
|
366
|
-
disk.l1_table = self.l1_table
|
|
367
|
-
disk.seek(0)
|
|
368
|
-
return disk
|
|
369
|
-
|
|
370
|
-
@cached_property
|
|
371
|
-
def l1_table(self) -> list[int]:
|
|
372
|
-
# L1 table is usually relatively small, it can be at most 32MB on PB or EB size disks
|
|
373
|
-
self.qcow2.fh.seek(self.header.l1_table_offset)
|
|
374
|
-
return c_qcow2.uint64[self.header.l1_size](self.qcow2.fh)
|
|
375
|
-
|
|
376
|
-
|
|
377
440
|
def offset_into_cluster(qcow2: QCow2, offset: int) -> int:
|
|
378
441
|
return offset & (qcow2.cluster_size - 1)
|
|
379
442
|
|
|
@@ -471,16 +534,16 @@ def get_subcluster_range_type(
|
|
|
471
534
|
sc_mask = (1 << sc_from) - 1
|
|
472
535
|
if sc_type == QCow2SubclusterType.QCOW2_SUBCLUSTER_NORMAL:
|
|
473
536
|
val = l2_bitmap | sc_mask # QCOW_OFLAG_SUB_ALLOC_RANGE(0, sc_from)
|
|
474
|
-
return sc_type, ctz(val
|
|
537
|
+
return sc_type, ctz(val) - sc_from
|
|
475
538
|
if sc_type in ZERO_SUBCLUSTER_TYPES:
|
|
476
539
|
val = (l2_bitmap | sc_mask) >> 32 # QCOW_OFLAG_SUB_ZERO_RANGE(0, sc_from)
|
|
477
|
-
return sc_type, ctz(val
|
|
540
|
+
return sc_type, ctz(val) - sc_from
|
|
478
541
|
if sc_type in UNALLOCATED_SUBCLUSTER_TYPES:
|
|
479
542
|
# We need to mask it with a 64bit mask because Python flips the sign bit
|
|
480
543
|
inv_mask = ~sc_mask & ((1 << 64) - 1) # ~QCOW_OFLAG_SUB_ALLOC_RANGE(0, sc_from)
|
|
481
544
|
|
|
482
545
|
val = ((l2_bitmap >> 32) | l2_bitmap) & inv_mask
|
|
483
|
-
return sc_type, ctz(val
|
|
546
|
+
return sc_type, ctz(val) - sc_from
|
|
484
547
|
|
|
485
548
|
raise Error(f"Invalid subcluster type: {sc_type}")
|
|
486
549
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.hypervisor
|
|
3
|
-
Version: 3.19.
|
|
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.19.
|
|
30
|
-
dissect_hypervisor-3.19.
|
|
31
|
-
dissect_hypervisor-3.19.
|
|
32
|
-
dissect_hypervisor-3.19.
|
|
33
|
-
dissect_hypervisor-3.19.
|
|
34
|
-
dissect_hypervisor-3.19.
|
|
35
|
-
dissect_hypervisor-3.19.
|
|
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,,
|
|
File without changes
|
{dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/COPYRIGHT
RENAMED
|
File without changes
|
{dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{dissect_hypervisor-3.19.dev1.dist-info → dissect_hypervisor-3.19.dev2.dist-info}/top_level.txt
RENAMED
|
File without changes
|