dissect.archive 1.2.dev2__tar.gz → 1.3.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. dissect_archive-1.3.dev1/.gitattributes +1 -0
  2. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/PKG-INFO +5 -2
  3. dissect_archive-1.3.dev1/dissect/archive/c_vbk.py +290 -0
  4. dissect_archive-1.3.dev1/dissect/archive/c_vma.py +60 -0
  5. dissect_archive-1.3.dev1/dissect/archive/exceptions.py +22 -0
  6. dissect_archive-1.3.dev1/dissect/archive/tools/__init__.py +0 -0
  7. dissect_archive-1.3.dev1/dissect/archive/tools/backup.py +208 -0
  8. dissect_archive-1.3.dev1/dissect/archive/vbk.py +1071 -0
  9. dissect_archive-1.3.dev1/dissect/archive/vma.py +269 -0
  10. dissect_archive-1.3.dev1/dissect/archive/xva.py +136 -0
  11. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect.archive.egg-info/PKG-INFO +5 -2
  12. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect.archive.egg-info/SOURCES.txt +15 -1
  13. dissect_archive-1.3.dev1/dissect.archive.egg-info/entry_points.txt +4 -0
  14. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect.archive.egg-info/requires.txt +5 -1
  15. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/pyproject.toml +10 -1
  16. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/tests/conftest.py +15 -0
  17. dissect_archive-1.3.dev1/tests/data/test.vma.gz +0 -0
  18. dissect_archive-1.3.dev1/tests/data/test13.vbk.gz +0 -0
  19. dissect_archive-1.3.dev1/tests/data/test9.vbk.gz +0 -0
  20. dissect_archive-1.3.dev1/tests/test_exceptions.py +19 -0
  21. dissect_archive-1.3.dev1/tests/test_vbk.py +125 -0
  22. dissect_archive-1.3.dev1/tests/test_vma.py +65 -0
  23. dissect_archive-1.2.dev2/.gitattributes +0 -1
  24. dissect_archive-1.2.dev2/dissect/archive/exceptions.py +0 -18
  25. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/COPYRIGHT +0 -0
  26. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/LICENSE +0 -0
  27. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/MANIFEST.in +0 -0
  28. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/README.md +0 -0
  29. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect/archive/__init__.py +0 -0
  30. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect/archive/c_wim.py +0 -0
  31. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect/archive/wim.py +0 -0
  32. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect.archive.egg-info/dependency_links.txt +0 -0
  33. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/dissect.archive.egg-info/top_level.txt +0 -0
  34. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/setup.cfg +0 -0
  35. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/tests/data/basic.wim.gz +0 -0
  36. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/tests/test_wim.py +0 -0
  37. {dissect_archive-1.2.dev2 → dissect_archive-1.3.dev1}/tox.ini +0 -0
@@ -0,0 +1 @@
1
+ tests/data/* filter=lfs diff=lfs merge=lfs -text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.archive
3
- Version: 1.2.dev2
3
+ Version: 1.3.dev1
4
4
  Summary: A Dissect module implementing parsers for various archive and backup formats
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -22,9 +22,12 @@ Requires-Python: ~=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  License-File: COPYRIGHT
25
- Requires-Dist: dissect.cstruct<5,>=4.dev
25
+ Requires-Dist: dissect.cstruct<5,>=4
26
26
  Requires-Dist: dissect.util<4,>=3
27
+ Provides-Extra: full
28
+ Requires-Dist: rich; extra == "full"
27
29
  Provides-Extra: dev
30
+ Requires-Dist: dissect.archive[full]; extra == "dev"
28
31
  Requires-Dist: dissect.cstruct<5.0.dev,>=4.0.dev; extra == "dev"
29
32
  Requires-Dist: dissect.util<4.0.dev,>=3.0.dev; extra == "dev"
30
33
 
@@ -0,0 +1,290 @@
1
+ from dissect.cstruct import cstruct
2
+
3
+ vbk_def = """
4
+ #define PAGE_SIZE 4096
5
+
6
+ /* Storage header */
7
+
8
+ struct StorageHeader {
9
+ uint32 FormatVersion; /* 0x0000 */
10
+ uint32 Initialized; /* 0x0004 */
11
+ uint32 DigestTypeLength; /* 0x0008 */
12
+ char DigestType[251]; /* 0x000C */
13
+ uint32 SnapshotSlotFormat; /* 0x0107 format > 5 -> crc32c */
14
+ uint32 StandardBlockSize; /* 0x010B */
15
+ uint8 ClusterAlign; /* 0x010F */
16
+ char Unk0[16]; /* 0x0120 */
17
+ char ExternalStorageId[16]; /* 0x0130 */
18
+ };
19
+
20
+ /* Snapshot header */
21
+
22
+ struct SnapshotSlotHeader {
23
+ uint32 CRC;
24
+ uint32 ContainsSnapshot;
25
+ };
26
+
27
+ struct DirectoryRootRecord {
28
+ int64 RootPage; /* Root page of the directory */
29
+ uint64 Count; /* Number of children */
30
+ };
31
+
32
+ struct BlocksStoreHeader {
33
+ int64 RootPage; /* Root of the blocks store */
34
+ uint64 Count; /* Number of blocks store entries */
35
+ int64 FreeRootPage; /* Root of the free blocks tree */
36
+ int64 DeduplicationRootPage; /* Root of the deduplication tree */
37
+ int64 Unk0;
38
+ int64 Unk1;
39
+ };
40
+
41
+ struct CryptoStoreRecord {
42
+ int64 RootPage; /* Root of the crypto store */
43
+ };
44
+
45
+ struct SnapshotDescriptor {
46
+ uint64 Version; /* Acts as a sequence number, highest is active slot */
47
+ uint64 StorageEOF; /* End of file, aka file size */
48
+ uint32 BanksCount; /* Number of banks */
49
+ DirectoryRootRecord DirectoryRoot; /* Directory root record */
50
+ BlocksStoreHeader BlocksStore; /* Blocks store header */
51
+ CryptoStoreRecord CryptoStore; /* Crypto store record */
52
+ uint64 Unk0;
53
+ uint64 Unk1;
54
+ };
55
+
56
+ struct BankDescriptor {
57
+ uint32 CRC;
58
+ uint64 Offset;
59
+ uint32 Size;
60
+ };
61
+
62
+ struct BanksGrain {
63
+ uint32 MaxBanks;
64
+ uint32 StoredBanks;
65
+ // BankDescriptor Banks[StoredBanks];
66
+ };
67
+
68
+ /* Block headers */
69
+
70
+ struct BankHeader {
71
+ uint16 PageCount;
72
+ uint16 Flags;
73
+ char Unk0[3064];
74
+ uint64 Unk1;
75
+ char Unk2[1020];
76
+ };
77
+
78
+ struct BankHeaderV71 {
79
+ uint16 PageCount;
80
+ uint16 Flags; /* 2 == encrypted */
81
+ char Unk0[3072];
82
+ char KeySetId[16];
83
+ char Unk1[16];
84
+ char Unk2[16];
85
+ uint32 Unk3;
86
+ char Unk4[968];
87
+ };
88
+
89
+ struct MetaBlobHeader {
90
+ int64 NextPage;
91
+ int32 Unk0;
92
+ };
93
+
94
+ struct Lz4BlockHeader {
95
+ uint32 Magic; /* 0xF800000F */
96
+ uint32 CRC; /* CRC32C of the compressed data */
97
+ uint32 SourceSize;
98
+ };
99
+
100
+ /* DirItem headers */
101
+ struct BlocksVectorHeader {
102
+ uint64 RootPage;
103
+ uint64 Count;
104
+ };
105
+
106
+ struct SubFolderHeader {
107
+ uint64 RootPage; /* 0x94 */
108
+ uint32 Count; /* 0x9C */
109
+ char Data[32]; /* 0xA0 */
110
+ }; /* 0xC0 */
111
+
112
+ struct ExtFibHeader {
113
+ uint16 UpdateInProgress; /* 0x94 */
114
+ uint8 Unk3; /* 0x96 */
115
+ uint8 Format; /* 0x97 Bit 3 == 1 */
116
+ BlocksVectorHeader BlocksVector; /* 0x98 */
117
+ uint64 FibSize; /* 0xA8 */
118
+ uint64 Size; /* 0xB0 */
119
+ uint8 FsObjAttachState; /* 0xB8 */
120
+ char Data[7]; /* 0xB9 */
121
+ }; /* 0xC0 */
122
+
123
+ struct IntFibHeader {
124
+ uint16 UpdateInProgress; /* 0x94 */
125
+ uint8 Unk3; /* 0x96 */
126
+ uint8 Format; /* 0x97 Bit 3 == 1 */
127
+ BlocksVectorHeader BlocksVector; /* 0x98 */
128
+ uint64 FibSize; /* 0xA8 */
129
+ uint64 Size; /* 0xB0 */
130
+ uint8 FsObjAttachState; /* 0xB8 */
131
+ char Data[7]; /* 0xB9 */
132
+ }; /* 0xC0 */
133
+
134
+ struct PatchHeader {
135
+ uint32 Unk0; /* 0x94 */
136
+ BlocksVectorHeader BlocksVector; /* 0x98 */
137
+ uint64 FibSize; /* 0xA8 Source file size */
138
+ uint64 Unk4; /* 0xB0 */
139
+ char Data[8]; /* 0xB8 */
140
+ }; /* 0xC0 */
141
+
142
+ struct IncrementHeader {
143
+ uint32 Unk0; /* 0x94 */
144
+ BlocksVectorHeader BlocksVector; /* 0x98 */
145
+ uint64 FibSize; /* 0xA8 Original FIB size */
146
+ uint64 Unk4; /* 0xB0 */
147
+ char Data[8]; /* 0xB8 */
148
+ }; /* 0xC0 */
149
+
150
+ enum DirItemType : uint32 {
151
+ None = 0,
152
+ SubFolder = 1,
153
+ ExtFib = 2,
154
+ IntFib = 3,
155
+ Patch = 4,
156
+ Increment = 5,
157
+ };
158
+
159
+ struct DirItemRecord {
160
+ DirItemType Type; /* 0x00 */
161
+ uint32 NameLength; /* 0x04 */
162
+ char Name[128]; /* 0x08 */
163
+ int64 PropsRootPage; /* 0x88 */
164
+ uint32 Unk1; /* 0x90 */
165
+ union { /* 0x94 */
166
+ char Data[44];
167
+ SubFolderHeader SubFolder;
168
+ ExtFibHeader ExtFib;
169
+ IntFibHeader IntFib;
170
+ PatchHeader Patch;
171
+ IncrementHeader Increment;
172
+ };
173
+ };
174
+
175
+ /* Block descriptors */
176
+
177
+ flag BlockFlags : uint8 {
178
+ None = 0x00,
179
+ Updated = 0x01,
180
+ CommitInProgress = 0x02,
181
+ };
182
+
183
+ enum BlockLocationType : uint8 {
184
+ Normal = 0x00,
185
+ Sparse = 0x01,
186
+ Reserved = 0x02,
187
+ Archived = 0x03, /* CompressedSize | (CompressionType << 32) */
188
+ BlockInBlob = 0x04, /* BlockId? & 0x3FFFFFF | (BlobId << 26) | ((Offset >> 9) << 42) */
189
+ BlockInBlobReserved = 0x05, /* BlockId? | 0xFFFFFFFFFC000000 */
190
+ };
191
+
192
+ enum CompressionType : int8 {
193
+ Plain = -1,
194
+ RL = 2,
195
+ ZLH = 3,
196
+ ZLL = 4,
197
+ LZ4 = 7,
198
+ };
199
+
200
+ struct MetaTableDescriptor {
201
+ int64 RootPage;
202
+ uint64 BlockSize;
203
+ uint64 Count;
204
+ };
205
+
206
+ struct StgBlockDescriptor {
207
+ uint8 Format; /* Format != 4 == legacy */
208
+ uint32 UsageCounter;
209
+ uint64 Offset;
210
+ uint32 AllocatedSize;
211
+ uint8 Deduplication;
212
+ char Digest[16];
213
+ CompressionType CompressionType;
214
+ uint8 Unk0;
215
+ uint32 CompressedSize;
216
+ uint32 SourceSize;
217
+ };
218
+
219
+ struct StgBlockDescriptorV7 {
220
+ uint8 Format; /* Format != 4 == legacy */
221
+ uint32 UsageCounter;
222
+ uint64 Offset;
223
+ uint32 AllocatedSize;
224
+ uint8 Deduplication;
225
+ char Digest[16];
226
+ CompressionType CompressionType;
227
+ uint8 Unk0;
228
+ uint32 CompressedSize;
229
+ uint32 SourceSize;
230
+ char KeySetId[16];
231
+ };
232
+
233
+ struct FibBlockDescriptor {
234
+ uint32 BlockSize;
235
+ BlockLocationType Type;
236
+ char Digest[16];
237
+ // union {
238
+ // struct {
239
+ // uint32 ArchiveUsedSize;
240
+ // uint8 ArchiveCompressionType;
241
+ // uint8 Unk3;
242
+ // uint16 Unk4;
243
+ // } Archived;
244
+ // uint64 Offset;
245
+ // };
246
+ uint64 BlockId; /* For performance reasons we just put a uint64 here, but this is actually a union */
247
+ BlockFlags Flags;
248
+ };
249
+
250
+ struct FibBlockDescriptorV7 {
251
+ uint32 BlockSize;
252
+ BlockLocationType Type;
253
+ char Digest[16];
254
+ // union {
255
+ // struct {
256
+ // uint32 ArchiveUsedSize;
257
+ // uint8 ArchiveCompressionType;
258
+ // uint8 Unk3;
259
+ // uint16 Unk4;
260
+ // } Archived;
261
+ // uint64 Offset;
262
+ // };
263
+ uint64 BlockId; /* For performance reasons we just put a uint64 here, but this is actually a union */
264
+ BlockFlags Flags;
265
+ char KeySetId[16];
266
+ };
267
+
268
+ struct PatchBlockDescriptor {
269
+ };
270
+
271
+ struct PatchBlockDescriptorV7 {
272
+ };
273
+
274
+ /* Property dictionary */
275
+
276
+ enum PropertyType : int32 {
277
+ UInt32 = 1,
278
+ UInt64 = 2,
279
+ AString = 3,
280
+ WString = 4,
281
+ Binary = 5,
282
+ Boolean = 6,
283
+ End = -1,
284
+ };
285
+ """ # noqa: E501
286
+
287
+ c_vbk = cstruct().load(vbk_def)
288
+
289
+ PAGE_SIZE = c_vbk.PAGE_SIZE
290
+ """VBK page size."""
@@ -0,0 +1,60 @@
1
+ from dissect.cstruct import cstruct
2
+
3
+ vma_def = """
4
+ #define VMA_BLOCK_BITS 12
5
+ #define VMA_BLOCK_SIZE (1 << VMA_BLOCK_BITS)
6
+ #define VMA_CLUSTER_BITS (VMA_BLOCK_BITS + 4)
7
+ #define VMA_CLUSTER_SIZE (1 << VMA_CLUSTER_BITS)
8
+
9
+ #define VMA_EXTENT_HEADER_SIZE 512
10
+ #define VMA_BLOCKS_PER_EXTENT 59
11
+ #define VMA_MAX_CONFIGS 256
12
+
13
+ #define VMA_MAX_EXTENT_SIZE (VMA_EXTENT_HEADER_SIZE + VMA_CLUSTER_SIZE * VMA_BLOCKS_PER_EXTENT)
14
+
15
+ /* File Format Definitions */
16
+
17
+ struct VmaDeviceInfoHeader {
18
+ uint32 devname_ptr; /* offset into blob_buffer table */
19
+ uint32 reserved0;
20
+ uint64 size; /* device size in bytes */
21
+ uint64 reserved1;
22
+ uint64 reserved2;
23
+ };
24
+
25
+ struct VmaHeader {
26
+ char magic[4];
27
+ uint32 version;
28
+ char uuid[16];
29
+ int64 ctime;
30
+ char md5sum[16];
31
+
32
+ uint32 blob_buffer_offset;
33
+ uint32 blob_buffer_size;
34
+ uint32 header_size;
35
+
36
+ char _reserved1[1984];
37
+
38
+ uint32 config_names[VMA_MAX_CONFIGS]; /* offset into blob_buffer table */
39
+ uint32 config_data[VMA_MAX_CONFIGS]; /* offset into blob_buffer table */
40
+
41
+ char _reserved2[4];
42
+
43
+ VmaDeviceInfoHeader dev_info[256];
44
+ };
45
+
46
+ struct VmaExtentHeader {
47
+ char magic[4];
48
+ uint16 reserved1;
49
+ uint16 block_count;
50
+ char uuid[16];
51
+ char md5sum[16];
52
+ uint64 blockinfo[VMA_BLOCKS_PER_EXTENT];
53
+ };
54
+ """
55
+
56
+ c_vma = cstruct(endian=">").load(vma_def)
57
+
58
+
59
+ VMA_MAGIC = b"VMA\x00"
60
+ VMA_EXTENT_MAGIC = b"VMAE"
@@ -0,0 +1,22 @@
1
+ class Error(Exception):
2
+ pass
3
+
4
+
5
+ class InvalidHeaderError(Error):
6
+ pass
7
+
8
+
9
+ class FileNotFoundError(Error, FileNotFoundError):
10
+ pass
11
+
12
+
13
+ class IsADirectoryError(Error, IsADirectoryError):
14
+ pass
15
+
16
+
17
+ class NotADirectoryError(Error, NotADirectoryError):
18
+ pass
19
+
20
+
21
+ class NotAReparsePointError(Error):
22
+ pass
@@ -0,0 +1,208 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from dissect.archive.c_vma import c_vma
7
+ from dissect.archive.vbk import VBK, DirItem
8
+ from dissect.archive.vma import VMA, _iter_mask
9
+
10
+ try:
11
+ from rich.logging import RichHandler
12
+ from rich.progress import (
13
+ BarColumn,
14
+ DownloadColumn,
15
+ Progress,
16
+ TextColumn,
17
+ TimeRemainingColumn,
18
+ TransferSpeedColumn,
19
+ )
20
+
21
+ progress = Progress(
22
+ TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
23
+ BarColumn(bar_width=None),
24
+ "[progress.percentage]{task.percentage:>3.1f}%",
25
+ "•",
26
+ DownloadColumn(),
27
+ "•",
28
+ TransferSpeedColumn(),
29
+ "•",
30
+ TimeRemainingColumn(),
31
+ transient=True,
32
+ )
33
+ except ImportError:
34
+ RichHandler = logging.StreamHandler
35
+
36
+ class Progress:
37
+ def __init__(self):
38
+ self.filename = None
39
+ self.total = None
40
+
41
+ self._task_id = 0
42
+ self._info = {}
43
+
44
+ def __enter__(self):
45
+ pass
46
+
47
+ def __exit__(self, *args, **kwargs) -> None:
48
+ sys.stderr.write("\n")
49
+ sys.stderr.flush()
50
+
51
+ def add_task(self, name: str, filename: str, total: int, **kwargs) -> int:
52
+ task_id = self._task_id
53
+ self._task_id += 1
54
+
55
+ self._info[task_id] = {"filename": filename, "total": total, "position": 0}
56
+
57
+ return task_id
58
+
59
+ def update(self, task_id: int, advance: int) -> None:
60
+ self._info[task_id]["position"] += advance
61
+ self.draw()
62
+
63
+ def draw(self) -> None:
64
+ infos = []
65
+ for info in self._info.values():
66
+ infos.append(f"{info['filename']} {(info['position'] / info['total']) * 100:0.2f}%")
67
+ sys.stderr.write("\r" + " | ".join(infos))
68
+ sys.stderr.flush()
69
+
70
+ progress = Progress()
71
+
72
+
73
+ log = logging.getLogger(__name__)
74
+
75
+
76
+ def setup_logging(logger: logging.Logger, verbosity: int) -> None:
77
+ if verbosity == 1:
78
+ level = logging.ERROR
79
+ elif verbosity == 2:
80
+ level = logging.WARNING
81
+ elif verbosity == 3:
82
+ level = logging.INFO
83
+ elif verbosity >= 4:
84
+ level = logging.DEBUG
85
+ else:
86
+ level = logging.CRITICAL
87
+
88
+ handler = RichHandler()
89
+ handler.setFormatter(logging.Formatter("%(message)s"))
90
+ handler.setLevel(level)
91
+ logger.addHandler(handler)
92
+ logger.setLevel(level)
93
+
94
+
95
+ def extract_vma(vma: VMA, out_dir: Path) -> None:
96
+ log.info("Extracting config files")
97
+ for config_name, config_data in vma.configs().items():
98
+ out_file = out_dir.joinpath(config_name)
99
+
100
+ log.info("%s -> %s (%d bytes)", config_name, out_file, len(config_data))
101
+ out_file.write_bytes(config_data)
102
+
103
+ log.info("Extracting device data")
104
+ tasks = {}
105
+ handles = {}
106
+ for device in vma.devices():
107
+ task_id = progress.add_task("extract", filename=device.name, total=device.size)
108
+ tasks[device.id] = task_id
109
+ handles[device.id] = out_dir.joinpath(device.name).open("wb")
110
+
111
+ with progress:
112
+ try:
113
+ for extent in vma.extents():
114
+ vma.fh.seek(extent.data_offset)
115
+ for block_info in extent.header.blockinfo:
116
+ cluster_num = block_info & 0xFFFFFFFF
117
+ dev_id = (block_info >> 32) & 0xFF
118
+ mask = block_info >> (32 + 16)
119
+
120
+ if dev_id == 0:
121
+ continue
122
+
123
+ fh_out = handles[dev_id]
124
+ fh_out.seek(cluster_num * c_vma.VMA_CLUSTER_SIZE)
125
+
126
+ if mask == 0xFFFF:
127
+ fh_out.write(vma.fh.read(c_vma.VMA_CLUSTER_SIZE))
128
+ elif mask == 0:
129
+ fh_out.write(b"\x00" * c_vma.VMA_CLUSTER_SIZE)
130
+ else:
131
+ for allocated, count in _iter_mask(mask, 16):
132
+ if allocated:
133
+ fh_out.write(vma.fh.read(count * c_vma.VMA_BLOCK_SIZE))
134
+ else:
135
+ fh_out.write(b"\x00" * count * c_vma.VMA_BLOCK_SIZE)
136
+
137
+ progress.update(tasks[dev_id], advance=c_vma.VMA_CLUSTER_SIZE)
138
+ except Exception as e:
139
+ log.exception("Exception during extraction")
140
+ log.debug("", exc_info=e)
141
+ finally:
142
+ for handle in handles.values():
143
+ handle.close()
144
+
145
+
146
+ def extract_vbk(vbk: VBK, out_dir: Path) -> None:
147
+ def extract_directory(directory: DirItem, out_dir: Path) -> None:
148
+ out_dir.mkdir(exist_ok=True)
149
+ for entry in directory.iterdir():
150
+ out_path = out_dir.joinpath(entry.name)
151
+ if entry.is_dir():
152
+ extract_directory(entry, out_path)
153
+ else:
154
+ task_id = progress.add_task("extract", filename=entry.name, total=entry.size)
155
+ with entry.open() as fh_in, out_path.open("wb") as fh_out:
156
+ for chunk in iter(lambda: fh_in.read(vbk.block_size), b""):
157
+ fh_out.write(chunk)
158
+ progress.update(task_id, advance=len(chunk))
159
+
160
+ with progress:
161
+ try:
162
+ extract_directory(vbk.get("/"), out_dir)
163
+ except Exception as e:
164
+ log.exception("Exception during extraction")
165
+ log.debug("", exc_info=e)
166
+
167
+
168
+ def main() -> None:
169
+ parser = argparse.ArgumentParser(description="Hypervisor backup extractor")
170
+ parser.add_argument("input", type=Path, help="path to backup file")
171
+ parser.add_argument("-o", "--output", type=Path, required=True, help="path to output directory")
172
+ parser.add_argument("-v", "--verbose", action="count", default=3, help="increase output verbosity")
173
+ args = parser.parse_args()
174
+
175
+ setup_logging(log, args.verbose)
176
+
177
+ in_file = args.input.resolve()
178
+ if not in_file.exists():
179
+ log.error("Input file does not exist: %s", in_file)
180
+ parser.exit()
181
+
182
+ out_dir = args.output.resolve()
183
+ if not out_dir.exists():
184
+ log.error("Output path does not exist: %s", out_dir)
185
+ parser.exit()
186
+
187
+ if not out_dir.is_dir():
188
+ log.error("Output path is not a directory: %s", out_dir)
189
+ parser.exit()
190
+
191
+ with in_file.open("rb") as fh:
192
+ for klass, extract in ((VMA, extract_vma), (VBK, extract_vbk)):
193
+ try:
194
+ backup = klass(fh)
195
+ extract(backup, out_dir)
196
+ break
197
+ except Exception as e:
198
+ log.debug("Failed to extract using %s", klass.__name__, exc_info=e)
199
+ else:
200
+ log.error("Unknown backup format")
201
+ parser.exit()
202
+
203
+
204
+ if __name__ == "__main__":
205
+ try:
206
+ sys.exit(main())
207
+ except KeyboardInterrupt:
208
+ pass