Skip to content

Commit 29fc9ec

Browse files
committed
Address review comments
1 parent 8972dca commit 29fc9ec

3 files changed

Lines changed: 115 additions & 35 deletions

File tree

dissect/hypervisor/backup/vbk.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,17 @@
55

66
from functools import cached_property, lru_cache
77
from io import BytesIO
8-
from typing import BinaryIO, Generic, Iterator, Optional, TypeVar
8+
from typing import BinaryIO, Generic, Iterator, TypeVar
99
from zlib import crc32
1010

1111
from dissect.cstruct import Structure
12+
from dissect.hypervisor.backup.c_vbk import c_vbk
13+
from dissect.hypervisor.exceptions import Error
14+
from dissect.util.compression import lz4
1215
from dissect.util.crc32c import crc32c
1316
from dissect.util.stream import AlignedStream
1417
from dissect.util.xmemoryview import xmemoryview
1518

16-
try:
17-
from lz4.block import decompress as lz4_decompress
18-
except ImportError:
19-
from dissect.util.compression.lz4 import decompress as lz4_decompress
20-
21-
from dissect.hypervisor.backup.c_vbk import c_vbk
22-
from dissect.hypervisor.exceptions import Error
23-
2419
PAGE_SIZE = 4096
2520
"""VBK page size."""
2621

@@ -108,7 +103,7 @@ def read_meta_blob(self, page: int) -> bytes:
108103
"""
109104
return self.active_slot._get_meta_blob(page)
110105

111-
def get(self, path: str, item: Optional[DirItem] = None) -> DirItem:
106+
def get(self, path: str, item: DirItem | None = None) -> DirItem:
112107
"""Get a directory item from the VBK file."""
113108
item = item or self.root
114109

@@ -351,7 +346,7 @@ def size(self) -> int:
351346
raise VBKError(f"Size not available for {self!r}")
352347

353348
@cached_property
354-
def properties(self) -> Optional[PropertiesDictionary]:
349+
def properties(self) -> PropertiesDictionary | None:
355350
"""The properties of the directory item, if it has them."""
356351
if self.entry.PropsRootPage == -1:
357352
return None
@@ -360,25 +355,22 @@ def properties(self) -> Optional[PropertiesDictionary]:
360355

361356
def is_dir(self) -> bool:
362357
"""Return whether the directory item is a directory."""
363-
return isinstance(self, (RootDirectory, SubFolderItem))
358+
return False
364359

365360
def is_file(self) -> bool:
366361
"""Return whether the directory item is a file."""
367362
return self.is_internal_file() or self.is_external_file()
368363

369364
def is_internal_file(self) -> bool:
370365
"""Return whether the directory item is an internal file."""
371-
return isinstance(self, IntFibItem)
366+
return False
372367

373368
def is_external_file(self) -> bool:
374369
"""Return whether the directory item is an external file."""
375-
return isinstance(self, ExtFibItem)
370+
return False
376371

377372
def listdir(self) -> dict[str, DirItem]:
378373
"""Return a dictionary of the items in the directory."""
379-
if not self.is_dir():
380-
raise NotADirectoryError(f"{self!r} is not a directory")
381-
382374
return {item.name: item for item in self.iterdir()}
383375

384376
def iterdir(self) -> Iterator[DirItem]:
@@ -402,6 +394,9 @@ def __init__(self, vbk: VBK, page: int, count: int):
402394
def __repr__(self) -> str:
403395
return f"<RootDirectory root={self.root} count={self.count}>"
404396

397+
def is_dir(self) -> bool:
398+
return True
399+
405400
def iterdir(self) -> Iterator[DirItem]:
406401
yield from MetaVector(self.vbk, DirItem, self.root, self.count)
407402

@@ -422,6 +417,9 @@ def __init__(self, vbk: VBK, buf: bytes):
422417
def __repr__(self) -> str:
423418
return f"<SubFolderItem name={self.name!r} root={self.root} count={self.count}>"
424419

420+
def is_dir(self) -> bool:
421+
return True
422+
425423
def iterdir(self) -> Iterator[DirItem]:
426424
yield from MetaVector(self.vbk, DirItem, self.root, self.count)
427425

@@ -441,6 +439,9 @@ def __repr__(self) -> str:
441439
def size(self) -> int:
442440
return self.entry.ExtFib.FibSize
443441

442+
def is_external_file(self) -> bool:
443+
return True
444+
444445

445446
class IntFibItem(DirItem):
446447
"""Directory item for an internal file.
@@ -460,6 +461,9 @@ def __repr__(self) -> str:
460461
def size(self) -> int:
461462
return self.entry.IntFib.FibSize
462463

464+
def is_internal_file(self) -> bool:
465+
return True
466+
463467
def open(self) -> FibStream:
464468
return FibStream(
465469
self.vbk,
@@ -477,13 +481,13 @@ class PatchItem(DirItem):
477481
- **TODO**: CPatchMeta
478482
"""
479483

484+
def __repr__(self) -> str:
485+
return f"<PatchItem name={self.name!r} size={self.size}>"
486+
480487
@cached_property
481488
def size(self) -> int:
482489
return self.entry.Patch.FibSize
483490

484-
def __repr__(self) -> str:
485-
return f"<PatchItem name={self.name!r} size={self.size}>"
486-
487491

488492
class IncrementItem(DirItem):
489493
"""Directory item for an increment.
@@ -493,13 +497,13 @@ class IncrementItem(DirItem):
493497
- **TODO**: CIncrementMeta
494498
"""
495499

500+
def __repr__(self) -> str:
501+
return f"<IncrementItem name={self.name!r} size={self.size}>"
502+
496503
@cached_property
497504
def size(self) -> int:
498505
return self.entry.Increment.FibSize
499506

500-
def __repr__(self) -> str:
501-
return f"<IncrementItem name={self.name!r} size={self.size}>"
502-
503507

504508
class MetaTableDescriptor(MetaItem):
505509
"""A descriptor for a meta table in the VBK file.
@@ -1030,7 +1034,7 @@ def _read(self, offset: int, length: int) -> bytes:
10301034
if block.is_compressed():
10311035
if block.compression_type == c_vbk.CompressionType.LZ4:
10321036
# First 12 bytes are Lz4BlockHeader
1033-
buf = lz4_decompress(memoryview(buf)[12:], block.source_size)
1037+
buf = lz4.decompress(memoryview(buf)[12:], block.source_size)
10341038
else:
10351039
raise VBKError(f"Unsupported compression type: {block.compression_type}")
10361040

dissect/hypervisor/tools/backup.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55

66
from dissect.hypervisor.backup.c_vma import c_vma
7-
from dissect.hypervisor.backup.vbk import VBK
7+
from dissect.hypervisor.backup.vbk import VBK, DirItem
88
from dissect.hypervisor.backup.vma import VMA, _iter_mask
99

1010
try:
@@ -144,20 +144,22 @@ def extract_vma(vma: VMA, out_dir: Path) -> None:
144144

145145

146146
def extract_vbk(vbk: VBK, out_dir: Path) -> None:
147-
with progress:
148-
try:
149-
root_dir = next(vbk.get("/").iterdir())
150-
out_dir = out_dir.joinpath(root_dir.name)
151-
out_dir.mkdir(exist_ok=True)
152-
153-
for entry in root_dir.iterdir():
154-
out_file = out_dir.joinpath(entry.name)
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:
155154
task_id = progress.add_task("extract", filename=entry.name, total=entry.size)
156-
157-
with entry.open() as fh_in, out_file.open("wb") as fh_out:
155+
with entry.open() as fh_in, out_path.open("wb") as fh_out:
158156
for chunk in iter(lambda: fh_in.read(vbk.block_size), b""):
159157
fh_out.write(chunk)
160158
progress.update(task_id, advance=len(chunk))
159+
160+
with progress:
161+
try:
162+
extract_directory(vbk.get("/"), out_dir)
161163
except Exception as e:
162164
log.exception("Exception during extraction")
163165
log.debug("", exc_info=e)

tests/test_backup.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import gzip
2+
import hashlib
3+
from pathlib import Path
4+
from typing import IO
5+
from unittest.mock import patch
6+
7+
import pytest
8+
9+
from dissect.hypervisor.tools.backup import main
10+
11+
12+
@pytest.mark.parametrize(
13+
"filename, expected",
14+
[
15+
(
16+
"test9.vbk.gz",
17+
[
18+
(
19+
"6745a759-2205-4cd2-b172-8ec8f7e60ef8 (78a5467d-87f5-8540-9a84-7569ae2849ad_2d1bb20f-49c1-485d-a689-696693713a5a)/DEV__dev_nvme1n1",
20+
"337350cac29d2ed34c23ce9fc675950badf85fd2b694791abe6999d36f0dc1b3",
21+
),
22+
(
23+
"6745a759-2205-4cd2-b172-8ec8f7e60ef8 (78a5467d-87f5-8540-9a84-7569ae2849ad_2d1bb20f-49c1-485d-a689-696693713a5a)/summary.xml",
24+
"d2b8f4d08e57a44b817b57d9c03e670c292e5a21e91fb5895b51e923781175e8",
25+
),
26+
],
27+
),
28+
(
29+
"test13.vbk.gz",
30+
[
31+
(
32+
"6745a759-2205-4cd2-b172-8ec8f7e60ef8 (3c834d56-37ac-8bd3-b946-30113c55c4b5)/BackupComponents.xml",
33+
"a9615b1cbce437074235ac194681d42e1f018f1c804277293dc24d4dd90eb504",
34+
),
35+
(
36+
"6745a759-2205-4cd2-b172-8ec8f7e60ef8 (3c834d56-37ac-8bd3-b946-30113c55c4b5)/digest_47d9f323-442b-433d-bd4f-1ecb3fa97351",
37+
"d6f9dced7c58628a4648e1a5ed349609f11cefb0ac6721c35c5f943ac18aaf10",
38+
),
39+
(
40+
"6745a759-2205-4cd2-b172-8ec8f7e60ef8 (3c834d56-37ac-8bd3-b946-30113c55c4b5)/GuestMembers.xml",
41+
"18228ae41c1e7ddb23ee6cfe49c9e2c1cdfe99393b45f20d7fbbdb5d247938b4",
42+
),
43+
(
44+
"6745a759-2205-4cd2-b172-8ec8f7e60ef8 (3c834d56-37ac-8bd3-b946-30113c55c4b5)/summary.xml",
45+
"c93e3460a4495a96047fd8c1c80c3169782207e106cc3b5f5d9b5edae68eb3d9",
46+
),
47+
(
48+
"6745a759-2205-4cd2-b172-8ec8f7e60ef8 (3c834d56-37ac-8bd3-b946-30113c55c4b5)/8b14f74c-360d-4d7a-98f7-7f4c5e737eb7",
49+
"e9ed281cf9c2fe1745e4eb9c926c1a64bd47569c48be511c5fdf6fd5793e5a77",
50+
),
51+
],
52+
),
53+
],
54+
)
55+
def test_backup_tool(
56+
filename: str, expected: list[tuple[str, str]], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
57+
) -> None:
58+
Path_open = Path.open
59+
60+
def mock_open(self, mode: str, *args, **kwargs) -> IO:
61+
if filename.endswith(".gz") and self.name == filename:
62+
return gzip.open(self, mode)
63+
64+
return Path_open(self, mode, *args, **kwargs)
65+
66+
with patch.object(Path, "open", mock_open):
67+
with monkeypatch.context() as m:
68+
m.setattr("sys.argv", ["backup-extract", f"tests/data/{filename}", "-o", str(tmp_path)])
69+
main()
70+
71+
for name, digest in expected:
72+
out_path = tmp_path / name
73+
assert out_path.exists()
74+
assert hashlib.sha256(out_path.read_bytes()).hexdigest() == digest

0 commit comments

Comments
 (0)