Skip to content

Commit 97430e3

Browse files
committed
Improve MetaVector2 lookup logic and add MetaBlob class
1 parent 29fc9ec commit 97430e3

2 files changed

Lines changed: 137 additions & 71 deletions

File tree

dissect/hypervisor/backup/vbk.py

Lines changed: 93 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@ class NotADirectoryError(VBKError):
3535
class VBK:
3636
"""Veeam Backup (VBK) file implementation.
3737
38-
Args:
39-
fh: The file handle of the VBK file to read.
40-
verify: Whether to verify checksums.
41-
4238
References:
4339
- CMeta
4440
- CStgFormat
4541
4642
Notes:
4743
- **TODO**: Encryption
4844
- **TODO**: Incrememental backups
45+
46+
Args:
47+
fh: The file handle of the VBK file to read.
48+
verify: Whether to verify checksums.
4949
"""
5050

5151
def __init__(self, fh: BinaryIO, verify: bool = True):
@@ -95,7 +95,7 @@ def page(self, idx: int) -> bytes:
9595
"""
9696
return self.active_slot.page(idx)
9797

98-
def read_meta_blob(self, page: int) -> bytes:
98+
def get_meta_blob(self, page: int) -> MetaBlob:
9999
"""Read a meta blob from the VBK file.
100100
101101
Args:
@@ -124,10 +124,6 @@ def get(self, path: str, item: DirItem | None = None) -> DirItem:
124124
class SnapshotSlot:
125125
"""A snapshot slot in the VBK file.
126126
127-
Args:
128-
vbk: The VBK object that the snapshot slot is part of.
129-
offset: The offset of the snapshot slot in the file.
130-
131127
References:
132128
- CSlotHdr
133129
- SSnapshotDescriptor
@@ -142,6 +138,10 @@ class SnapshotSlot:
142138
- **TODO**: Free blocks index (CFreeBlocksIndex, SFreeBlockIndexItem)
143139
- **TODO**: Deduplication index (CDedupIndex, SDedupIndexItem)
144140
- **TODO**: Crypto store (CCryptoStore, SCryptoStoreRec)
141+
142+
Args:
143+
vbk: The VBK object that the snapshot slot is part of.
144+
offset: The offset of the snapshot slot in the file.
145145
"""
146146

147147
def __init__(self, vbk: VBK, offset: int):
@@ -214,41 +214,26 @@ def page(self, page: int) -> bytes:
214214
"""
215215
return self.banks[page >> 32].page(page & 0xFFFFFFFF)
216216

217-
def _get_meta_blob(self, page: int) -> bytes:
218-
"""Read a meta blob from the snapshot slot.
219-
220-
A meta blob is a list of pages that are linked together. Each page has a header (``MetaBlobHeader``) with
221-
a ``NextPage`` field that points to the next page in the blob. The last page has a ``NextPage`` field of -1.
217+
def _get_meta_blob(self, page: int) -> MetaBlob:
218+
"""Get a meta blob from the snapshot slot.
222219
223220
Args:
224221
page: The page of the first page in the meta blob.
225-
226-
References:
227-
- CMetaBlobRW
228222
"""
229-
result = []
230-
231-
while page != -1:
232-
buf = self.page(page)
233-
result.append(buf)
234-
235-
# Read the next page from the header
236-
page = int.from_bytes(buf[:8], "little", signed=True)
237-
238-
return b"".join(result)
223+
return MetaBlob(self, page)
239224

240225

241226
class Bank:
242227
"""A bank in the snapshot slot. A bank is a collection of pages.
243228
229+
References:
230+
- SBankHdr
231+
- CBankHdrPage
232+
244233
Args:
245234
vbk: The VBK object that the bank is part of.
246235
offset: The offset of the bank in the file.
247236
size: The size of the bank in the file.
248-
249-
References:
250-
- SBankHdr
251-
- CBankHdrPage
252237
"""
253238

254239
def __init__(self, vbk: VBK, offset: int, size: int):
@@ -748,20 +733,20 @@ def keyset_id(self) -> bytes:
748733
class PropertiesDictionary(dict):
749734
"""A dictionary of properties in the VBK file.
750735
751-
Args:
752-
vbk: The VBK object that the properties dictionary is part of.
753-
page: The page number of the meta blob of the properties dictionary.
754-
755736
References:
756737
- CPropsDictionary
757738
- CDirElemPropsRW
739+
740+
Args:
741+
vbk: The VBK object that the properties dictionary is part of.
742+
page: The page number of the meta blob of the properties dictionary.
758743
"""
759744

760745
def __init__(self, vbk: VBK, page: int):
761746
self.vbk = vbk
762747
self.page = page
763748

764-
buf = BytesIO(self.vbk.read_meta_blob(page))
749+
buf = BytesIO(self.vbk.get_meta_blob(page).data())
765750
buf.seek(len(c_vbk.MetaBlobHeader))
766751

767752
while True:
@@ -790,20 +775,57 @@ def __init__(self, vbk: VBK, page: int):
790775
self[name] = value
791776

792777

778+
class MetaBlob:
779+
"""A meta blob in the VBK file.
780+
781+
A meta blob is a list of pages that are linked together. Each page has a header (``MetaBlobHeader``) with
782+
a ``NextPage`` field that points to the next page in the blob. The last page has a ``NextPage`` field of -1.
783+
784+
References:
785+
- CMetaBlobRW
786+
787+
Args:
788+
slot: The snapshot slot that the meta blob is part of.
789+
root: The page number of the first page in the meta blob.
790+
"""
791+
792+
def __init__(self, slot: SnapshotSlot, root: int):
793+
self.slot = slot
794+
self.root = root
795+
796+
def __repr__(self) -> str:
797+
return f"<MetaBlob root={self.root}>"
798+
799+
def _read(self) -> Iterator[int, memoryview]:
800+
page = self.root
801+
802+
while page != -1:
803+
buf = self.slot.page(page)
804+
yield page, buf
805+
806+
page = int.from_bytes(buf[:8], "little", signed=True)
807+
808+
def pages(self) -> Iterator[int]:
809+
return (page for page, _ in self._read())
810+
811+
def data(self) -> bytes:
812+
return b"".join(buf for _, buf in self._read())
813+
814+
793815
T = TypeVar("T", bound=MetaItem)
794816

795817

796818
class MetaVector(Generic[T]):
797819
"""A vector of meta items in the VBK file.
798820
821+
References:
822+
- CMetaVec
823+
799824
Args:
800825
vbk: The VBK object that the vector is part of.
801826
type_: The type of the items in the vector.
802827
page: The page number of the first page in the vector.
803828
count: The number of items in the vector.
804-
805-
References:
806-
- CMetaVec
807829
"""
808830

809831
def __new__(cls, vbk: VBK, *args, **kwargs):
@@ -819,13 +841,7 @@ def __init__(self, vbk: VBK, type_: type[T], page: int, count: int):
819841

820842
self._entry_size = len(self.type.__struct__)
821843
self._entries_per_page = PAGE_SIZE // self._entry_size
822-
self._pages = []
823-
824-
while page != -1:
825-
self._pages.append(page)
826-
827-
buf = self.vbk.page(page)
828-
page = int.from_bytes(buf[:8], "little", signed=True)
844+
self._pages = list(self.vbk.get_meta_blob(page).pages())
829845

830846
self.get = lru_cache(128)(self.get)
831847

@@ -863,27 +879,33 @@ def get(self, idx: int) -> T:
863879
class MetaVector2(MetaVector[T]):
864880
"""A vector of meta items in the VBK file. Version 2.
865881
882+
MetaVector2 is essentially a table of page numbers that contain the vector entries.
883+
The table pages of a MetaVector2 have a 8-32 byte header, so we can hold a maximum of 508-511 entries per page.
884+
Read the comments in _lookup_page for more information.
885+
886+
References:
887+
- CMetaVec2
888+
866889
Args:
867890
vbk: The VBK object that the vector is part of.
868891
type_: The type of the items in the vector.
869892
page: The page number of the first page in the vector.
870893
count: The number of items in the vector.
871-
872-
References:
873-
- CMetaVec2
874894
"""
875895

876-
# MetaVector2 is essentially a table of page numbers that contain the vector entries
877-
# The table pages of a MetaVector2 have a 8-16 byte header, so we can hold a maximum of 510-511 entries per page
878-
# Read the comments in _lookup_page for more information
879-
MAX_TABLE_ENTRIES_PER_PAGE = (PAGE_SIZE - 8) // 8
896+
_MAX_TABLE_ENTRIES_PER_PAGE = PAGE_SIZE // 8
897+
_MAX_TABLE_ENTRIES_LOOKUP = (
898+
_MAX_TABLE_ENTRIES_PER_PAGE - 1,
899+
_MAX_TABLE_ENTRIES_PER_PAGE - 4,
900+
_MAX_TABLE_ENTRIES_PER_PAGE - 1,
901+
)
880902

881903
def __init__(self, vbk: VBK, type_: type[T], page: int, count: int):
882904
super().__init__(vbk, type_, page, count)
883905

884906
# It's not actually a meta blob, but the same mechanism is used (next page pointer in the header)
885907
# The table itself is essentially a big array of 64 bit integers, so cast it to a memoryview of that
886-
self._table = xmemoryview(self.vbk.read_meta_blob(page), "<q")
908+
self._table = xmemoryview(self.vbk.get_meta_blob(page).data(), "<q")
887909
self._lookup_page = lru_cache(128)(self._lookup_page)
888910

889911
def _lookup_page(self, idx: int) -> int:
@@ -892,7 +914,6 @@ def _lookup_page(self, idx: int) -> int:
892914
Args:
893915
idx: The page index to lookup the page number for.
894916
"""
895-
page_id = idx + 1
896917

897918
# MetaVec2 pages are a little special
898919
# The first page has a 16 byte header:
@@ -909,20 +930,22 @@ def _lookup_page(self, idx: int) -> int:
909930
# We've not seen a table large enough to see this repeat more than once, but presumably it does
910931
#
911932
# This means that the first page can hold 510 entries, the second 508, and the third and fourth 511 each
912-
# Reverse engineering gives us this funny looking loop, but it works, so use it for now
913-
if page_id > self.MAX_TABLE_ENTRIES_PER_PAGE - 1:
914-
page_id_offset = 1
915-
while True:
916-
prev_page_id_offset = page_id_offset
917-
page_id_offset *= 4
918-
919-
if (4 * (self.MAX_TABLE_ENTRIES_PER_PAGE - 1)) * prev_page_id_offset >= page_id:
920-
break
933+
# The fifth page can hold 508 entries again, and so on
934+
935+
if idx < self._MAX_TABLE_ENTRIES_PER_PAGE - 2:
936+
return self._table[idx + 2] # Skip the header
921937

922-
page_id = page_id_offset + idx
938+
idx -= self._MAX_TABLE_ENTRIES_PER_PAGE - 2
939+
table_idx = 1
940+
while True:
941+
max_entries = self._MAX_TABLE_ENTRIES_LOOKUP[table_idx % 3]
923942

924-
table_id, page_id_in_table = divmod(page_id, self.MAX_TABLE_ENTRIES_PER_PAGE)
925-
return self._table[table_id * (PAGE_SIZE // 8) + 1 + page_id_in_table]
943+
if idx < max_entries:
944+
table_offset = table_idx * self._MAX_TABLE_ENTRIES_PER_PAGE
945+
return self._table[table_offset + (self._MAX_TABLE_ENTRIES_PER_PAGE - max_entries) + idx]
946+
947+
idx -= max_entries
948+
table_idx += 1
926949

927950
def data(self, idx: int) -> bytes:
928951
"""Read the data for an entry in the vector.
@@ -940,13 +963,13 @@ def data(self, idx: int) -> bytes:
940963
class FibMetaSparseTable:
941964
"""A sparse table of FIB (File In Backup) blocks in the VBK file.
942965
966+
References:
967+
- CFibMetaSparseTable
968+
943969
Args:
944970
vbk: The VBK object that the sparse table is part of.
945971
page: The page number of the first page in the table.
946972
count: The number of entries in the table.
947-
948-
References:
949-
- CFibMetaSparseTable
950973
"""
951974

952975
# This seems hardcoded? Probably calculated from something but unknown for now

tests/test_vbk.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import hashlib
2+
import struct
23
from typing import BinaryIO
4+
from unittest.mock import Mock
35

4-
from dissect.hypervisor.backup.vbk import VBK, MetaVector, MetaVector2
6+
from dissect.hypervisor.backup.vbk import PAGE_SIZE, VBK, MetaBlob, MetaTableDescriptor, MetaVector, MetaVector2
57

68

79
def test_vbk_version_9(vbk9: BinaryIO) -> None:
@@ -73,3 +75,44 @@ def test_vbk_version_13(vbk13: BinaryIO) -> None:
7375
with entry.open() as fh:
7476
digest = hashlib.sha256(fh.read()).hexdigest()
7577
assert digest == "e9ed281cf9c2fe1745e4eb9c926c1a64bd47569c48be511c5fdf6fd5793e5a77"
78+
79+
80+
def test_metavector2_lookup() -> None:
81+
"""test that the lookup logic in MetaVector2 works as expected"""
82+
83+
tmp = []
84+
entry = 0
85+
num_pages = 11
86+
for i in range(num_pages):
87+
if i == 0:
88+
# root page
89+
header = struct.pack("<qq", i + 1, 0)
90+
elif i % 3 == 1:
91+
# "first" page
92+
header = struct.pack(
93+
"<qqqq",
94+
i + 1 if i + 1 < num_pages else -1,
95+
i,
96+
i + 1 if i + 1 < num_pages else -1,
97+
i + 2 if i + 2 < num_pages else -1,
98+
)
99+
else:
100+
# "middle" pages
101+
header = struct.pack("<q", i + 1 if i + 1 < num_pages else -1)
102+
103+
num_entries = (PAGE_SIZE // 8) - (len(header) // 8)
104+
data = struct.pack(f"<{num_entries}q", *range(entry, entry + num_entries))
105+
tmp.append(header + data)
106+
entry += num_entries
107+
108+
mock_blob = b"".join(tmp)
109+
110+
mock_slot = Mock()
111+
mock_slot.page = lambda page: mock_blob[page * PAGE_SIZE : (page + 1) * PAGE_SIZE]
112+
mock_vbk = Mock(format_version=13)
113+
mock_vbk.get_meta_blob = lambda page: MetaBlob(mock_slot, page)
114+
vec = MetaVector2(mock_vbk, MetaTableDescriptor, 0, 4096)
115+
assert isinstance(vec, MetaVector2)
116+
117+
for i in range(entry):
118+
assert vec._lookup_page(i) == i

0 commit comments

Comments
 (0)