Skip to content

Commit bcfbe50

Browse files
committed
Cleanup ASIF calculations
1 parent 9736c31 commit bcfbe50

1 file changed

Lines changed: 61 additions & 46 deletions

File tree

dissect/hypervisor/disk/asif.py

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ class ASIF:
1818
framework, as well as through Disk Utility.
1919
2020
An ASIF file is pretty straight forward. There's a small header which, among some other details, contains two
21-
directory offsets. Each directory contains a list of tables, which in turn contain a list of data entries. Each data
22-
entry points to a chunk of data in the ASIF file. The chunk size is defined in the header and is typically 1 MiB.
23-
The chunk size is always a multiple of the block size, which is also defined in the header (typically 512 bytes).
21+
directory offsets. Each directory contains a list of tables, and each table contains a list of data and bitmap
22+
entries. Each data entry points to a chunk of data in the ASIF file, and each bitmap entry points to a bitmap that
23+
covers a group of data entries. We call the combined number of data entries plus the bitmap entry a "chunk group".
24+
25+
The chunk size is defined in the header and is typically 1 MiB. The chunk size is always a multiple of the block
26+
size, which is also defined in the header (typically 512 bytes).
27+
2428
Each directory has a version number, and the directory with the highest version number is the active directory. This
2529
allows for atomic updates of the directory/table data.
2630
@@ -55,33 +59,35 @@ def __init__(self, fh: BinaryIO):
5559
self.size = self.header.sector_count * self.block_size
5660
self.max_size = self.header.max_sector_count * self.block_size
5761

58-
# The following math is taken from the assembly with some creative variable naming
59-
# It's possible that some of this can be simplified or the names improved
6062
self._blocks_per_chunk = self.chunk_size // self.block_size
6163

62-
# This check doesn't really make sense, but keep it in for now
63-
reserved_size = 4 * self.chunk_size
64-
self._num_reserved_table_entries = (
65-
1 if reserved_size < self._blocks_per_chunk else reserved_size // self._blocks_per_chunk
66-
)
64+
# Table entries are grouped into "chunk groups", with each group followed by a bitmap that covers that group
65+
# We need to calculate how large a chunk group is, and how many chunk groups we can fit in a table
6766

68-
self._max_table_entries = self.chunk_size >> 3
69-
self._num_table_entries = self._max_table_entries - (
70-
self._max_table_entries % (self._num_reserved_table_entries + 1)
71-
)
72-
self._num_reserved_directory_entries = (self._num_reserved_table_entries + self._num_table_entries) // (
73-
self._num_reserved_table_entries + 1
74-
)
75-
self._num_usable_entries = self._num_table_entries - self._num_reserved_directory_entries
76-
# This is the size in bytes of data covered by a single table
77-
self._size_per_table = self._num_usable_entries * self.chunk_size
67+
# A bitmap uses 2 bits per block (not chunk), so a single byte in the bitmap covers 4 blocks
68+
# A bitmap is 1 chunk large in bytes, so a single bitmap can cover 4 * chunk_size blocks
69+
# A bitmap covers one chunk group, so the number of blocks per chunk group is equal to the bitmap coverage
70+
num_blocks_per_group = 4 * self.chunk_size
7871

79-
max_size = self.block_size * self.header.max_sector_count
80-
self._num_directory_entries = (self._size_per_table + max_size - 1) // self._size_per_table
72+
# Derive the number of chunks per group from this
73+
self._num_chunks_per_group = max(1, num_blocks_per_group // self._blocks_per_chunk)
8174

82-
self._aligned_table_size = (
83-
(self.block_size + 8 * self._num_table_entries - 1) // self.block_size * self.block_size
84-
)
75+
# A table is 1 chunk large, so we start with the number of entries (uint64) that fit inside a single chunk
76+
num_words_per_chunk = self.chunk_size // 8
77+
# A chunk group has one entry for each chunk, plus one entry for the bitmap
78+
num_entries_per_group = self._num_chunks_per_group + 1
79+
80+
num_groups_per_table, num_remaining = divmod(num_words_per_chunk, num_entries_per_group)
81+
# The number of total entries in a table, including both chunk and bitmap entries
82+
self._num_table_entries = num_words_per_chunk - num_remaining
83+
84+
# Calculate the size in bytes of data covered by a single table
85+
num_chunk_entries_per_table = self._num_table_entries - num_groups_per_table
86+
self._size_per_table = num_chunk_entries_per_table * self.chunk_size
87+
88+
# Calculate the maximum size of the virtual disk, and the number of tables needed to cover that size
89+
max_size = self.block_size * self.header.max_sector_count
90+
self._num_tables = (self._size_per_table + max_size - 1) // self._size_per_table
8591

8692
self.directories = sorted(
8793
(Directory(self, offset) for offset in self.header.directory_offsets),
@@ -167,17 +173,17 @@ def entries(self) -> list[int]:
167173
"""List of table entries in the directory."""
168174
# Seek over the version
169175
self.asif.fh.seek(self.offset + 8)
170-
return c_asif.uint64[self.asif._num_directory_entries](self.asif.fh)
176+
return c_asif.uint64[self.asif._num_tables](self.asif.fh)
171177

172178
def table(self, index: int) -> Table:
173179
"""Get a table from the directory.
174180
175181
Args:
176182
index: Index of the table in the directory.
177183
"""
178-
if index >= self.asif._num_directory_entries:
184+
if index >= self.asif._num_tables:
179185
raise IndexError("Table index out of range")
180-
return Table(self, index)
186+
return Table(self.asif, index, self.entries[index] * self.asif.chunk_size)
181187

182188

183189
class Table:
@@ -186,27 +192,36 @@ class Table:
186192
A table contains a list of data entries (``uint64[]``). Each data entry is a chunk number and points to a chunk of
187193
data in the ASIF image. Each table covers a fixed amount of data in the virtual disk.
188194
189-
Data entries have 55 bits usable for the chunk number and 9 bits reserved for flags.
195+
Data entries have 55 bits usable for the chunk number and 9 bits for flags, 7 of which are reserved.
190196
191197
.. rubric :: Encoding
192198
.. code-block:: c
193199
194200
0b00000000 01111111 11111111 11111111 11111111 11111111 11111111 11111111 (chunk number)
195201
0b00111111 10000000 00000000 00000000 00000000 00000000 00000000 00000000 (reserved)
196-
0b01000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 (entry dirty)
197-
0b10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 (content dirty)
202+
0b11000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 (flags)
203+
204+
The following flags are known for data entries:
205+
206+
.. rubric :: Flags
207+
.. code-block:: c
208+
209+
0b00 (uninitialized)
210+
0b01 (fully initialized)
211+
0b10 (unmapped)
212+
0b11 (has bitmap)
198213
199214
Args:
200-
directory: The directory this table belongs to.
215+
asif: The ASIF image this table belongs to.
201216
index: Index of the table in the directory.
217+
offset: Offset of the table in the ASIF image.
202218
"""
203219

204-
def __init__(self, directory: Directory, index: int):
205-
self.asif = directory.asif
206-
self.directory = directory
220+
def __init__(self, asif: ASIF, index: int, offset: int):
221+
self.asif = asif
207222
self.index = index
223+
self.offset = offset
208224

209-
self.offset = self.directory.entries[index] * self.asif.chunk_size
210225
self.virtual_offset = index * self.asif._size_per_table
211226

212227
def __repr__(self) -> str:
@@ -237,23 +252,23 @@ def _read(self, offset: int, length: int) -> bytes:
237252
result = []
238253
while length:
239254
table = self.directory.table(offset // self.asif._size_per_table)
255+
256+
# Calculate the relative chunk index within the table
240257
relative_block_index = (offset // self.asif.block_size) - (table.virtual_offset // self.asif.block_size)
241-
data_idx = (
242-
relative_block_index // self.asif._blocks_per_chunk
243-
+ relative_block_index // self.asif._blocks_per_chunk * self.asif._num_reserved_table_entries
244-
) // self.asif._num_reserved_table_entries
258+
relative_chunk_index = relative_block_index // self.asif._blocks_per_chunk
259+
260+
# Calculate the chunk group
261+
chunk_group = relative_chunk_index // self.asif._num_chunks_per_group
262+
# Each chunk group has a bitmap entry, so we need to account for that in the entry index
263+
entry_index = relative_chunk_index + chunk_group
245264

246-
# 0x8000000000000000 = content dirty bit
247-
# 0x4000000000000000 = entry dirty bit
248-
# 0x3F80000000000000 = reserved bits
249-
chunk = table.entries[data_idx] & 0x7FFFFFFFFFFFFF
250-
raw_offset = chunk * self.asif.chunk_size
265+
chunk = table.entries[entry_index] & 0x7FFFFFFFFFFFFF
251266

252267
read_length = min(length, self.asif.chunk_size)
253268
if chunk == 0:
254269
result.append(b"\x00" * read_length)
255270
else:
256-
self.asif.fh.seek(raw_offset)
271+
self.asif.fh.seek(chunk * self.asif.chunk_size)
257272
result.append(self.asif.fh.read(read_length))
258273

259274
offset += read_length

0 commit comments

Comments
 (0)