@@ -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
183189class 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