Skip to main content

rust_hdf5/format/chunk_index/
fixed_array.rs

1//! Fixed Array (FA) chunk index structures for HDF5.
2//!
3//! Implements the on-disk format for the fixed array used to index chunked
4//! datasets where no dimension is unlimited (all dimensions are fixed-size).
5//!
6//! Structures:
7//!   - Header (FAHD): metadata about the fixed array
8//!   - Data Block (FADB): holds chunk addresses (or filtered chunk entries)
9
10use crate::format::bytes::{read_le_addr as read_addr, read_le_uint as read_size};
11use crate::format::checksum::checksum_metadata;
12use crate::format::{FormatContext, FormatError, FormatResult, UNDEF_ADDR};
13
14/// Signature for the fixed array header.
15pub const FAHD_SIGNATURE: [u8; 4] = *b"FAHD";
16/// Signature for the fixed array data block.
17pub const FADB_SIGNATURE: [u8; 4] = *b"FADB";
18
19/// Fixed array version.
20pub const FA_VERSION: u8 = 0;
21
22/// Client ID for unfiltered chunks.
23pub const FA_CLIENT_CHUNK: u8 = 0;
24/// Client ID for filtered chunks.
25pub const FA_CLIENT_FILT_CHUNK: u8 = 1;
26
27/// Default `log2(max elements per data block page)`.
28///
29/// Matches libhdf5's `H5D_FARRAY_MAX_DBLK_PAGE_NELMTS_BITS` (`H5Dpkg.h`),
30/// i.e. 1024 elements per page. libhdf5 asserts this value is `> 0`
31/// (`H5Dfarray.c`), so 0 is never a valid on-disk value.
32pub const FA_MAX_DBLK_PAGE_NELMTS_BITS: u8 = 10;
33
34/// Fixed array header.
35///
36/// On-disk layout:
37/// ```text
38/// "FAHD"(4) + version=0(1) + client_id(1)
39/// + element_size(1) + max_dblk_page_nelmts_bits(1)
40/// + num_elmts(sizeof_size)
41/// + data_blk_addr(sizeof_addr)
42/// + checksum(4)
43/// ```
44#[derive(Debug, Clone, PartialEq)]
45pub struct FixedArrayHeader {
46    pub client_id: u8,
47    pub element_size: u8,
48    pub max_dblk_page_nelmts_bits: u8,
49    pub num_elmts: u64,
50    pub data_blk_addr: u64,
51}
52
53impl FixedArrayHeader {
54    /// Create a new header for unfiltered chunk indexing.
55    pub fn new_for_chunks(ctx: &FormatContext, num_elmts: u64) -> Self {
56        Self {
57            client_id: FA_CLIENT_CHUNK,
58            element_size: ctx.sizeof_addr,
59            max_dblk_page_nelmts_bits: FA_MAX_DBLK_PAGE_NELMTS_BITS,
60            num_elmts,
61            data_blk_addr: UNDEF_ADDR,
62        }
63    }
64
65    /// Create a new header for filtered chunk indexing.
66    ///
67    /// `chunk_size_len` is the number of bytes needed to encode the chunk size
68    /// (typically computed from the maximum possible compressed chunk size).
69    pub fn new_for_filtered_chunks(
70        ctx: &FormatContext,
71        num_elmts: u64,
72        chunk_size_len: u8,
73    ) -> Self {
74        // element_size = sizeof_addr + chunk_size_len + 4 (filter_mask)
75        let element_size = ctx.sizeof_addr + chunk_size_len + 4;
76        Self {
77            client_id: FA_CLIENT_FILT_CHUNK,
78            element_size,
79            max_dblk_page_nelmts_bits: FA_MAX_DBLK_PAGE_NELMTS_BITS,
80            num_elmts,
81            data_blk_addr: UNDEF_ADDR,
82        }
83    }
84
85    /// Number of elements per data block page (`1 << max_dblk_page_nelmts_bits`).
86    ///
87    /// Mirrors `dblk_page_nelmts` in libhdf5 (`H5FAcache.c`).
88    pub fn dblk_page_nelmts(&self) -> u64 {
89        1u64 << (self.max_dblk_page_nelmts_bits as u64)
90    }
91
92    /// Whether the data block for this array is paged.
93    ///
94    /// A data block is paged when `num_elmts > dblk_page_nelmts`
95    /// (`H5FAcache.c`: `if (hdr->cparam.nelmts > dblk_page_nelmts)`).
96    pub fn is_paged(&self) -> bool {
97        self.num_elmts > self.dblk_page_nelmts()
98    }
99
100    /// Number of pages in the (paged) data block.
101    ///
102    /// `npages = ceil(num_elmts / dblk_page_nelmts)` — encoded in C as
103    /// `((nelmts + dblk_page_nelmts) - 1) / dblk_page_nelmts`.
104    /// Returns 0 when the data block is not paged.
105    pub fn npages(&self) -> u64 {
106        if self.is_paged() {
107            self.num_elmts.div_ceil(self.dblk_page_nelmts())
108        } else {
109            0
110        }
111    }
112
113    /// Compute the encoded size (for pre-allocation).
114    pub fn encoded_size(&self, ctx: &FormatContext) -> usize {
115        let ss = ctx.sizeof_size as usize;
116        let sa = ctx.sizeof_addr as usize;
117        // signature(4) + version(1) + client_id(1)
118        // + element_size(1) + max_dblk_page_nelmts_bits(1)
119        // + num_elmts(sizeof_size) + data_blk_addr(sizeof_addr)
120        // + checksum(4)
121        4 + 1 + 1 + 1 + 1 + ss + sa + 4
122    }
123
124    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
125        let ss = ctx.sizeof_size as usize;
126        let sa = ctx.sizeof_addr as usize;
127        let size = self.encoded_size(ctx);
128        let mut buf = Vec::with_capacity(size);
129
130        buf.extend_from_slice(&FAHD_SIGNATURE);
131        buf.push(FA_VERSION);
132        buf.push(self.client_id);
133        buf.push(self.element_size);
134        buf.push(self.max_dblk_page_nelmts_bits);
135
136        buf.extend_from_slice(&self.num_elmts.to_le_bytes()[..ss]);
137        buf.extend_from_slice(&self.data_blk_addr.to_le_bytes()[..sa]);
138
139        let cksum = checksum_metadata(&buf);
140        buf.extend_from_slice(&cksum.to_le_bytes());
141
142        debug_assert_eq!(buf.len(), size);
143        buf
144    }
145
146    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<Self> {
147        let ss = ctx.sizeof_size as usize;
148        let sa = ctx.sizeof_addr as usize;
149        let min_size = 4 + 1 + 1 + 1 + 1 + ss + sa + 4;
150
151        if buf.len() < min_size {
152            return Err(FormatError::BufferTooShort {
153                needed: min_size,
154                available: buf.len(),
155            });
156        }
157
158        if buf[0..4] != FAHD_SIGNATURE {
159            return Err(FormatError::InvalidSignature);
160        }
161
162        let version = buf[4];
163        if version != FA_VERSION {
164            return Err(FormatError::InvalidVersion(version));
165        }
166
167        // Verify checksum
168        let data_end = min_size - 4;
169        let stored_cksum = u32::from_le_bytes([
170            buf[data_end],
171            buf[data_end + 1],
172            buf[data_end + 2],
173            buf[data_end + 3],
174        ]);
175        let computed_cksum = checksum_metadata(&buf[..data_end]);
176        if stored_cksum != computed_cksum {
177            return Err(FormatError::ChecksumMismatch {
178                expected: stored_cksum,
179                computed: computed_cksum,
180            });
181        }
182
183        let client_id = buf[5];
184        let element_size = buf[6];
185        let max_dblk_page_nelmts_bits = buf[7];
186        // Bound the page-bits field: `1 << bits` is used as the page size,
187        // so a value >= 64 would overflow the shift. libhdf5 only writes 10.
188        if max_dblk_page_nelmts_bits >= 64 {
189            return Err(FormatError::InvalidData(format!(
190                "fixed-array max_dblk_page_nelmts_bits {max_dblk_page_nelmts_bits} is too large"
191            )));
192        }
193
194        let mut pos = 8;
195        let num_elmts = read_size(&buf[pos..], ss);
196        pos += ss;
197        let data_blk_addr = read_addr(&buf[pos..], sa);
198
199        Ok(Self {
200            client_id,
201            element_size,
202            max_dblk_page_nelmts_bits,
203            num_elmts,
204            data_blk_addr,
205        })
206    }
207}
208
209/// A single element in a fixed array for unfiltered chunks.
210/// Each element is simply a chunk address (sizeof_addr bytes).
211#[derive(Debug, Clone, PartialEq)]
212pub struct FixedArrayChunkElement {
213    pub address: u64,
214}
215
216/// A single element in a fixed array for filtered chunks.
217#[derive(Debug, Clone, PartialEq)]
218pub struct FixedArrayFilteredChunkElement {
219    pub address: u64,
220    /// Stored (on-disk) chunk size. `u64` because the encoded field is
221    /// `chunk_size_len` bytes wide — up to 8 under a version-5 layout (or a
222    /// v4 layout whose uncompressed chunk derives an 8-byte width).
223    pub chunk_size: u64,
224    pub filter_mask: u32,
225}
226
227/// Fixed array data block.
228///
229/// On-disk layout:
230/// ```text
231/// "FADB"(4) + version=0(1) + client_id(1)
232/// + header_addr(sizeof_addr)
233/// + [if paged: page_init_bitmap]
234/// + elements(num_elmts * element_size)
235/// + checksum(4)
236/// ```
237#[derive(Debug, Clone, PartialEq)]
238pub struct FixedArrayDataBlock {
239    pub client_id: u8,
240    pub header_addr: u64,
241    /// Chunk addresses (for unfiltered chunks).
242    pub elements: Vec<u64>,
243    /// Filtered chunk entries (for filtered chunks; only used when client_id == 1).
244    pub filtered_elements: Vec<FixedArrayFilteredChunkElement>,
245}
246
247impl FixedArrayDataBlock {
248    /// Create a new empty data block for unfiltered chunks.
249    pub fn new_unfiltered(header_addr: u64, num_elmts: usize) -> Self {
250        Self {
251            client_id: FA_CLIENT_CHUNK,
252            header_addr,
253            elements: vec![UNDEF_ADDR; num_elmts],
254            filtered_elements: Vec::new(),
255        }
256    }
257
258    /// Create a new empty data block for filtered chunks.
259    pub fn new_filtered(header_addr: u64, num_elmts: usize) -> Self {
260        let default_entry = FixedArrayFilteredChunkElement {
261            address: UNDEF_ADDR,
262            chunk_size: 0,
263            filter_mask: 0,
264        };
265        Self {
266            client_id: FA_CLIENT_FILT_CHUNK,
267            header_addr,
268            elements: Vec::new(),
269            filtered_elements: vec![default_entry; num_elmts],
270        }
271    }
272
273    /// Compute the encoded size for unfiltered chunks.
274    pub fn encoded_size_unfiltered(&self, ctx: &FormatContext) -> usize {
275        let sa = ctx.sizeof_addr as usize;
276        // signature(4) + version(1) + client_id(1)
277        // + header_addr(sa)
278        // + elements(n * sa)
279        // + checksum(4)
280        4 + 1 + 1 + sa + self.elements.len() * sa + 4
281    }
282
283    /// Compute the encoded size for filtered chunks.
284    pub fn encoded_size_filtered(&self, ctx: &FormatContext, chunk_size_len: usize) -> usize {
285        let sa = ctx.sizeof_addr as usize;
286        let elem_size = sa + chunk_size_len + 4; // addr + chunk_size + filter_mask
287                                                 // signature(4) + version(1) + client_id(1)
288                                                 // + header_addr(sa)
289                                                 // + elements(n * elem_size)
290                                                 // + checksum(4)
291        4 + 1 + 1 + sa + self.filtered_elements.len() * elem_size + 4
292    }
293
294    /// Encode for unfiltered chunks.
295    pub fn encode_unfiltered(&self, ctx: &FormatContext) -> Vec<u8> {
296        let sa = ctx.sizeof_addr as usize;
297        let size = self.encoded_size_unfiltered(ctx);
298        let mut buf = Vec::with_capacity(size);
299
300        buf.extend_from_slice(&FADB_SIGNATURE);
301        buf.push(FA_VERSION);
302        buf.push(self.client_id);
303        buf.extend_from_slice(&self.header_addr.to_le_bytes()[..sa]);
304
305        for &addr in &self.elements {
306            buf.extend_from_slice(&addr.to_le_bytes()[..sa]);
307        }
308
309        let cksum = checksum_metadata(&buf);
310        buf.extend_from_slice(&cksum.to_le_bytes());
311
312        debug_assert_eq!(buf.len(), size);
313        buf
314    }
315
316    /// Encode for filtered chunks.
317    pub fn encode_filtered(&self, ctx: &FormatContext, chunk_size_len: usize) -> Vec<u8> {
318        let sa = ctx.sizeof_addr as usize;
319        let size = self.encoded_size_filtered(ctx, chunk_size_len);
320        let mut buf = Vec::with_capacity(size);
321
322        buf.extend_from_slice(&FADB_SIGNATURE);
323        buf.push(FA_VERSION);
324        buf.push(self.client_id);
325        buf.extend_from_slice(&self.header_addr.to_le_bytes()[..sa]);
326
327        for elem in &self.filtered_elements {
328            buf.extend_from_slice(&elem.address.to_le_bytes()[..sa]);
329            buf.extend_from_slice(&elem.chunk_size.to_le_bytes()[..chunk_size_len]);
330            buf.extend_from_slice(&elem.filter_mask.to_le_bytes());
331        }
332
333        let cksum = checksum_metadata(&buf);
334        buf.extend_from_slice(&cksum.to_le_bytes());
335
336        debug_assert_eq!(buf.len(), size);
337        buf
338    }
339
340    /// Decode for unfiltered chunks.
341    pub fn decode_unfiltered(
342        buf: &[u8],
343        ctx: &FormatContext,
344        num_elmts: usize,
345    ) -> FormatResult<Self> {
346        let sa = ctx.sizeof_addr as usize;
347        // Saturating: `num_elmts` is file-derived; an overflowing product
348        // must yield a huge `min_size` (clean BufferTooShort), not wrap.
349        let min_size = num_elmts.saturating_mul(sa).saturating_add(10 + sa);
350
351        if buf.len() < min_size {
352            return Err(FormatError::BufferTooShort {
353                needed: min_size,
354                available: buf.len(),
355            });
356        }
357
358        if buf[0..4] != FADB_SIGNATURE {
359            return Err(FormatError::InvalidSignature);
360        }
361
362        let version = buf[4];
363        if version != FA_VERSION {
364            return Err(FormatError::InvalidVersion(version));
365        }
366
367        // Verify checksum
368        let data_end = min_size - 4;
369        let stored_cksum = u32::from_le_bytes([
370            buf[data_end],
371            buf[data_end + 1],
372            buf[data_end + 2],
373            buf[data_end + 3],
374        ]);
375        let computed_cksum = checksum_metadata(&buf[..data_end]);
376        if stored_cksum != computed_cksum {
377            return Err(FormatError::ChecksumMismatch {
378                expected: stored_cksum,
379                computed: computed_cksum,
380            });
381        }
382
383        let client_id = buf[5];
384        let mut pos = 6;
385        let header_addr = read_addr(&buf[pos..], sa);
386        pos += sa;
387
388        let mut elements = Vec::with_capacity(num_elmts);
389        for _ in 0..num_elmts {
390            elements.push(read_addr(&buf[pos..], sa));
391            pos += sa;
392        }
393
394        Ok(Self {
395            client_id,
396            header_addr,
397            elements,
398            filtered_elements: Vec::new(),
399        })
400    }
401
402    /// Decode for filtered chunks.
403    pub fn decode_filtered(
404        buf: &[u8],
405        ctx: &FormatContext,
406        num_elmts: usize,
407        chunk_size_len: usize,
408    ) -> FormatResult<Self> {
409        let sa = ctx.sizeof_addr as usize;
410        let elem_size = sa + chunk_size_len + 4;
411        let min_size = num_elmts.saturating_mul(elem_size).saturating_add(10 + sa);
412
413        if buf.len() < min_size {
414            return Err(FormatError::BufferTooShort {
415                needed: min_size,
416                available: buf.len(),
417            });
418        }
419
420        if buf[0..4] != FADB_SIGNATURE {
421            return Err(FormatError::InvalidSignature);
422        }
423
424        let version = buf[4];
425        if version != FA_VERSION {
426            return Err(FormatError::InvalidVersion(version));
427        }
428
429        // Verify checksum
430        let data_end = min_size - 4;
431        let stored_cksum = u32::from_le_bytes([
432            buf[data_end],
433            buf[data_end + 1],
434            buf[data_end + 2],
435            buf[data_end + 3],
436        ]);
437        let computed_cksum = checksum_metadata(&buf[..data_end]);
438        if stored_cksum != computed_cksum {
439            return Err(FormatError::ChecksumMismatch {
440                expected: stored_cksum,
441                computed: computed_cksum,
442            });
443        }
444
445        let client_id = buf[5];
446        let mut pos = 6;
447        let header_addr = read_addr(&buf[pos..], sa);
448        pos += sa;
449
450        let mut filtered_elements = Vec::with_capacity(num_elmts);
451        for _ in 0..num_elmts {
452            let address = read_addr(&buf[pos..], sa);
453            pos += sa;
454            let chunk_size = read_size(&buf[pos..], chunk_size_len);
455            pos += chunk_size_len;
456            let filter_mask =
457                u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
458            pos += 4;
459            filtered_elements.push(FixedArrayFilteredChunkElement {
460                address,
461                chunk_size,
462                filter_mask,
463            });
464        }
465
466        Ok(Self {
467            client_id,
468            header_addr,
469            elements: Vec::new(),
470            filtered_elements,
471        })
472    }
473}
474
475/// Parsed prefix of a *paged* fixed array data block.
476///
477/// On-disk layout (`H5FA_DBLOCK_PREFIX_SIZE` + checksum):
478/// ```text
479/// "FADB"(4) + version=0(1) + client_id(1)
480/// + header_addr(sizeof_addr)
481/// + page_init_bitmap(ceil(npages/8) bytes)
482/// + checksum(4)
483/// ```
484/// The `npages` pages follow at `dblk_addr + prefix_size`, where
485/// `prefix_size` is the full size including the checksum.
486#[derive(Debug, Clone, PartialEq)]
487pub struct FixedArrayPagedPrefix {
488    pub client_id: u8,
489    pub header_addr: u64,
490    /// Page-init bitmap, MSB-first: page `p` initialized iff
491    /// `bitmap[p / 8] & (0x80 >> (p % 8))` is non-zero.
492    pub page_init_bitmap: Vec<u8>,
493    /// Total size of the prefix on disk, including the 4-byte checksum.
494    /// Pages start at `dblk_addr + prefix_size`.
495    pub prefix_size: usize,
496}
497
498impl FixedArrayPagedPrefix {
499    /// Decode the prefix of a paged data block.
500    ///
501    /// `npages` comes from the header (`FixedArrayHeader::npages`).
502    pub fn decode(buf: &[u8], ctx: &FormatContext, npages: u64) -> FormatResult<Self> {
503        let sa = ctx.sizeof_addr as usize;
504        let bitmap_size = (npages as usize).div_ceil(8);
505        // signature(4) + version(1) + client_id(1) + header_addr(sa)
506        // + bitmap(bitmap_size) + checksum(4)
507        let prefix_size = 4 + 1 + 1 + sa + bitmap_size + 4;
508
509        if buf.len() < prefix_size {
510            return Err(FormatError::BufferTooShort {
511                needed: prefix_size,
512                available: buf.len(),
513            });
514        }
515
516        if buf[0..4] != FADB_SIGNATURE {
517            return Err(FormatError::InvalidSignature);
518        }
519
520        let version = buf[4];
521        if version != FA_VERSION {
522            return Err(FormatError::InvalidVersion(version));
523        }
524
525        // Verify checksum over everything before the trailing 4 bytes.
526        let data_end = prefix_size - 4;
527        let stored_cksum = u32::from_le_bytes([
528            buf[data_end],
529            buf[data_end + 1],
530            buf[data_end + 2],
531            buf[data_end + 3],
532        ]);
533        let computed_cksum = checksum_metadata(&buf[..data_end]);
534        if stored_cksum != computed_cksum {
535            return Err(FormatError::ChecksumMismatch {
536                expected: stored_cksum,
537                computed: computed_cksum,
538            });
539        }
540
541        let client_id = buf[5];
542        let mut pos = 6;
543        let header_addr = read_addr(&buf[pos..], sa);
544        pos += sa;
545        let page_init_bitmap = buf[pos..pos + bitmap_size].to_vec();
546
547        Ok(Self {
548            client_id,
549            header_addr,
550            page_init_bitmap,
551            prefix_size,
552        })
553    }
554
555    /// Whether page `p` has been initialized (MSB-first bit test).
556    ///
557    /// Mirrors `H5VM_bit_get`: `bitmap[p / 8] & (0x80 >> (p % 8))`.
558    pub fn page_initialized(&self, p: usize) -> bool {
559        let byte = p / 8;
560        if byte >= self.page_init_bitmap.len() {
561            return false;
562        }
563        (self.page_init_bitmap[byte] & (0x80u8 >> (p % 8))) != 0
564    }
565
566    /// Encode the prefix of a paged data block (matches `decode`).
567    ///
568    /// On-disk layout: `"FADB"(4) + version(1) + client_id(1)
569    /// + header_addr(sizeof_addr) + page_init_bitmap(ceil(npages/8)) + checksum(4)`.
570    /// The returned buffer is exactly `prefix_size` bytes.
571    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
572        let sa = ctx.sizeof_addr as usize;
573        let mut buf = Vec::with_capacity(4 + 1 + 1 + sa + self.page_init_bitmap.len() + 4);
574        buf.extend_from_slice(&FADB_SIGNATURE);
575        buf.push(FA_VERSION);
576        buf.push(self.client_id);
577        buf.extend_from_slice(&self.header_addr.to_le_bytes()[..sa]);
578        buf.extend_from_slice(&self.page_init_bitmap);
579        let cksum = checksum_metadata(&buf);
580        buf.extend_from_slice(&cksum.to_le_bytes());
581        buf
582    }
583}
584
585/// Encode a single *unfiltered* data block page.
586///
587/// Layout: `addrs.len()` chunk addresses (`sizeof_addr` bytes each, MSB-padded
588/// little-endian) followed by a 4-byte Jenkins checksum over the elements.
589/// Mirrors `H5FA__cache_dblk_page_serialize` (`H5FAcache.c`).
590pub fn encode_unfiltered_page(addrs: &[u64], ctx: &FormatContext) -> Vec<u8> {
591    let sa = ctx.sizeof_addr as usize;
592    let mut buf = Vec::with_capacity(addrs.len() * sa + 4);
593    for &addr in addrs {
594        buf.extend_from_slice(&addr.to_le_bytes()[..sa]);
595    }
596    let cksum = checksum_metadata(&buf);
597    buf.extend_from_slice(&cksum.to_le_bytes());
598    buf
599}
600
601/// Encode a single *filtered* data block page.
602///
603/// Layout: per element `address(sizeof_addr) + chunk_size(chunk_size_len)
604/// + filter_mask(4)`, followed by a 4-byte Jenkins checksum over the elements.
605pub fn encode_filtered_page(
606    elems: &[FixedArrayFilteredChunkElement],
607    ctx: &FormatContext,
608    chunk_size_len: usize,
609) -> Vec<u8> {
610    let sa = ctx.sizeof_addr as usize;
611    let elem_size = sa + chunk_size_len + 4;
612    let mut buf = Vec::with_capacity(elems.len() * elem_size + 4);
613    for e in elems {
614        buf.extend_from_slice(&e.address.to_le_bytes()[..sa]);
615        buf.extend_from_slice(&e.chunk_size.to_le_bytes()[..chunk_size_len]);
616        buf.extend_from_slice(&e.filter_mask.to_le_bytes());
617    }
618    let cksum = checksum_metadata(&buf);
619    buf.extend_from_slice(&cksum.to_le_bytes());
620    buf
621}
622
623/// Decode the unfiltered chunk addresses contained in a single data block page.
624///
625/// `page_buf` must start at the page address and contain at least
626/// `nelmts * sizeof_addr + 4` bytes. The trailing 4 bytes are a Jenkins
627/// checksum over the page elements.
628pub fn decode_unfiltered_page(
629    page_buf: &[u8],
630    ctx: &FormatContext,
631    nelmts: usize,
632) -> FormatResult<Vec<u64>> {
633    let sa = ctx.sizeof_addr as usize;
634    // Saturating: `nelmts` is file-derived; an overflowing product must yield
635    // a huge `page_size` (clean BufferTooShort), not wrap.
636    let page_size = nelmts.saturating_mul(sa).saturating_add(4);
637    if page_buf.len() < page_size {
638        return Err(FormatError::BufferTooShort {
639            needed: page_size,
640            available: page_buf.len(),
641        });
642    }
643
644    let data_end = page_size - 4;
645    let stored_cksum = u32::from_le_bytes([
646        page_buf[data_end],
647        page_buf[data_end + 1],
648        page_buf[data_end + 2],
649        page_buf[data_end + 3],
650    ]);
651    let computed_cksum = checksum_metadata(&page_buf[..data_end]);
652    if stored_cksum != computed_cksum {
653        return Err(FormatError::ChecksumMismatch {
654            expected: stored_cksum,
655            computed: computed_cksum,
656        });
657    }
658
659    let mut elements = Vec::with_capacity(nelmts);
660    let mut pos = 0;
661    for _ in 0..nelmts {
662        elements.push(read_addr(&page_buf[pos..], sa));
663        pos += sa;
664    }
665    Ok(elements)
666}
667
668/// Decode the filtered chunk entries contained in a single data block page.
669///
670/// `page_buf` must start at the page address and contain at least
671/// `nelmts * (sizeof_addr + chunk_size_len + 4) + 4` bytes. The trailing 4
672/// bytes are a Jenkins checksum over the page elements.
673pub fn decode_filtered_page(
674    page_buf: &[u8],
675    ctx: &FormatContext,
676    nelmts: usize,
677    chunk_size_len: usize,
678) -> FormatResult<Vec<FixedArrayFilteredChunkElement>> {
679    let sa = ctx.sizeof_addr as usize;
680    let elem_size = sa + chunk_size_len + 4;
681    // Saturating: `nelmts` is file-derived; an overflowing product must yield
682    // a huge `page_size` (clean BufferTooShort), not wrap.
683    let page_size = nelmts.saturating_mul(elem_size).saturating_add(4);
684    if page_buf.len() < page_size {
685        return Err(FormatError::BufferTooShort {
686            needed: page_size,
687            available: page_buf.len(),
688        });
689    }
690
691    let data_end = page_size - 4;
692    let stored_cksum = u32::from_le_bytes([
693        page_buf[data_end],
694        page_buf[data_end + 1],
695        page_buf[data_end + 2],
696        page_buf[data_end + 3],
697    ]);
698    let computed_cksum = checksum_metadata(&page_buf[..data_end]);
699    if stored_cksum != computed_cksum {
700        return Err(FormatError::ChecksumMismatch {
701            expected: stored_cksum,
702            computed: computed_cksum,
703        });
704    }
705
706    let mut elements = Vec::with_capacity(nelmts);
707    let mut pos = 0;
708    for _ in 0..nelmts {
709        let address = read_addr(&page_buf[pos..], sa);
710        pos += sa;
711        let chunk_size = read_size(&page_buf[pos..], chunk_size_len);
712        pos += chunk_size_len;
713        let filter_mask = u32::from_le_bytes([
714            page_buf[pos],
715            page_buf[pos + 1],
716            page_buf[pos + 2],
717            page_buf[pos + 3],
718        ]);
719        pos += 4;
720        elements.push(FixedArrayFilteredChunkElement {
721            address,
722            chunk_size,
723            filter_mask,
724        });
725    }
726    Ok(elements)
727}
728
729// ========================================================================= helpers
730
731// ======================================================================= tests
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    fn ctx8() -> FormatContext {
738        FormatContext {
739            sizeof_addr: 8,
740            sizeof_size: 8,
741        }
742    }
743
744    fn ctx4() -> FormatContext {
745        FormatContext {
746            sizeof_addr: 4,
747            sizeof_size: 4,
748        }
749    }
750
751    #[test]
752    fn header_roundtrip() {
753        let mut hdr = FixedArrayHeader::new_for_chunks(&ctx8(), 10);
754        hdr.data_blk_addr = 0x2000;
755
756        let encoded = hdr.encode(&ctx8());
757        assert_eq!(encoded.len(), hdr.encoded_size(&ctx8()));
758        assert_eq!(&encoded[..4], b"FAHD");
759
760        let decoded = FixedArrayHeader::decode(&encoded, &ctx8()).unwrap();
761        assert_eq!(decoded, hdr);
762    }
763
764    #[test]
765    fn header_roundtrip_ctx4() {
766        let mut hdr = FixedArrayHeader::new_for_chunks(&ctx4(), 5);
767        hdr.data_blk_addr = 0x800;
768
769        let encoded = hdr.encode(&ctx4());
770        let decoded = FixedArrayHeader::decode(&encoded, &ctx4()).unwrap();
771        assert_eq!(decoded, hdr);
772    }
773
774    #[test]
775    fn header_bad_signature() {
776        let hdr = FixedArrayHeader::new_for_chunks(&ctx8(), 10);
777        let mut encoded = hdr.encode(&ctx8());
778        encoded[0] = b'X';
779        let err = FixedArrayHeader::decode(&encoded, &ctx8()).unwrap_err();
780        assert!(matches!(err, FormatError::InvalidSignature));
781    }
782
783    #[test]
784    fn header_checksum_mismatch() {
785        let hdr = FixedArrayHeader::new_for_chunks(&ctx8(), 10);
786        let mut encoded = hdr.encode(&ctx8());
787        encoded[6] ^= 0xFF;
788        let err = FixedArrayHeader::decode(&encoded, &ctx8()).unwrap_err();
789        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
790    }
791
792    #[test]
793    fn data_block_unfiltered_roundtrip() {
794        let mut dblk = FixedArrayDataBlock::new_unfiltered(0x1000, 4);
795        dblk.elements[0] = 0x3000;
796        dblk.elements[1] = 0x4000;
797        dblk.elements[2] = UNDEF_ADDR;
798        dblk.elements[3] = 0x5000;
799
800        let encoded = dblk.encode_unfiltered(&ctx8());
801        assert_eq!(encoded.len(), dblk.encoded_size_unfiltered(&ctx8()));
802        assert_eq!(&encoded[..4], b"FADB");
803
804        let decoded = FixedArrayDataBlock::decode_unfiltered(&encoded, &ctx8(), 4).unwrap();
805        assert_eq!(decoded.elements, dblk.elements);
806        assert_eq!(decoded.header_addr, 0x1000);
807    }
808
809    #[test]
810    fn data_block_unfiltered_roundtrip_ctx4() {
811        let mut dblk = FixedArrayDataBlock::new_unfiltered(0x500, 3);
812        dblk.elements[0] = 0x100;
813        dblk.elements[1] = 0x200;
814        dblk.elements[2] = 0x300;
815
816        let encoded = dblk.encode_unfiltered(&ctx4());
817        let decoded = FixedArrayDataBlock::decode_unfiltered(&encoded, &ctx4(), 3).unwrap();
818        assert_eq!(decoded.elements, dblk.elements);
819    }
820
821    #[test]
822    fn data_block_unfiltered_bad_checksum() {
823        let dblk = FixedArrayDataBlock::new_unfiltered(0x1000, 2);
824        let mut encoded = dblk.encode_unfiltered(&ctx8());
825        encoded[8] ^= 0xFF;
826        let err = FixedArrayDataBlock::decode_unfiltered(&encoded, &ctx8(), 2).unwrap_err();
827        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
828    }
829
830    #[test]
831    fn data_block_filtered_roundtrip() {
832        let mut dblk = FixedArrayDataBlock::new_filtered(0x1000, 2);
833        dblk.filtered_elements[0] = FixedArrayFilteredChunkElement {
834            address: 0x2000,
835            chunk_size: 512,
836            filter_mask: 0,
837        };
838        dblk.filtered_elements[1] = FixedArrayFilteredChunkElement {
839            address: 0x3000,
840            chunk_size: 400,
841            filter_mask: 1,
842        };
843
844        let chunk_size_len = 4; // 4 bytes for chunk_size
845        let encoded = dblk.encode_filtered(&ctx8(), chunk_size_len);
846        assert_eq!(
847            encoded.len(),
848            dblk.encoded_size_filtered(&ctx8(), chunk_size_len)
849        );
850
851        let decoded =
852            FixedArrayDataBlock::decode_filtered(&encoded, &ctx8(), 2, chunk_size_len).unwrap();
853        assert_eq!(decoded.filtered_elements, dblk.filtered_elements);
854    }
855
856    #[test]
857    fn header_filtered_roundtrip() {
858        let hdr = FixedArrayHeader::new_for_filtered_chunks(&ctx8(), 6, 4);
859        assert_eq!(hdr.element_size, 8 + 4 + 4); // addr + chunk_size_len + filter_mask
860        assert_eq!(hdr.client_id, FA_CLIENT_FILT_CHUNK);
861
862        let encoded = hdr.encode(&ctx8());
863        let decoded = FixedArrayHeader::decode(&encoded, &ctx8()).unwrap();
864        assert_eq!(decoded, hdr);
865    }
866
867    #[test]
868    fn empty_data_block() {
869        let dblk = FixedArrayDataBlock::new_unfiltered(0x500, 0);
870        let encoded = dblk.encode_unfiltered(&ctx8());
871        let decoded = FixedArrayDataBlock::decode_unfiltered(&encoded, &ctx8(), 0).unwrap();
872        assert!(decoded.elements.is_empty());
873    }
874
875    // ----------------------------------------------------------------- paging
876
877    #[test]
878    fn header_paging_geometry() {
879        // max_dblk_page_nelmts_bits = 2 => 4 elements per page.
880        let mut hdr = FixedArrayHeader::new_for_chunks(&ctx8(), 10);
881        hdr.max_dblk_page_nelmts_bits = 2;
882        assert_eq!(hdr.dblk_page_nelmts(), 4);
883        assert!(hdr.is_paged()); // 10 > 4
884        assert_eq!(hdr.npages(), 3); // ceil(10 / 4)
885
886        // num_elmts == dblk_page_nelmts => NOT paged (strict greater-than).
887        hdr.num_elmts = 4;
888        assert!(!hdr.is_paged());
889        assert_eq!(hdr.npages(), 0);
890
891        // One past the page size => paged with 2 pages.
892        hdr.num_elmts = 5;
893        assert!(hdr.is_paged());
894        assert_eq!(hdr.npages(), 2);
895    }
896
897    /// Build a paged FADB prefix buffer for `npages` pages.
898    fn build_paged_prefix(
899        ctx: &FormatContext,
900        client_id: u8,
901        header_addr: u64,
902        npages: usize,
903        init_bits: &[bool],
904    ) -> Vec<u8> {
905        let sa = ctx.sizeof_addr as usize;
906        let bitmap_size = npages.div_ceil(8);
907        let mut bitmap = vec![0u8; bitmap_size];
908        for (p, &on) in init_bits.iter().enumerate() {
909            if on {
910                bitmap[p / 8] |= 0x80u8 >> (p % 8);
911            }
912        }
913        let mut buf = Vec::new();
914        buf.extend_from_slice(&FADB_SIGNATURE);
915        buf.push(FA_VERSION);
916        buf.push(client_id);
917        buf.extend_from_slice(&header_addr.to_le_bytes()[..sa]);
918        buf.extend_from_slice(&bitmap);
919        let cksum = checksum_metadata(&buf);
920        buf.extend_from_slice(&cksum.to_le_bytes());
921        buf
922    }
923
924    /// Build a single unfiltered data block page.
925    fn build_unfiltered_page(ctx: &FormatContext, addrs: &[u64]) -> Vec<u8> {
926        let sa = ctx.sizeof_addr as usize;
927        let mut buf = Vec::new();
928        for &a in addrs {
929            buf.extend_from_slice(&a.to_le_bytes()[..sa]);
930        }
931        let cksum = checksum_metadata(&buf);
932        buf.extend_from_slice(&cksum.to_le_bytes());
933        buf
934    }
935
936    /// Build a single filtered data block page.
937    fn build_filtered_page(
938        ctx: &FormatContext,
939        chunk_size_len: usize,
940        elems: &[FixedArrayFilteredChunkElement],
941    ) -> Vec<u8> {
942        let sa = ctx.sizeof_addr as usize;
943        let mut buf = Vec::new();
944        for e in elems {
945            buf.extend_from_slice(&e.address.to_le_bytes()[..sa]);
946            buf.extend_from_slice(&e.chunk_size.to_le_bytes()[..chunk_size_len]);
947            buf.extend_from_slice(&e.filter_mask.to_le_bytes());
948        }
949        let cksum = checksum_metadata(&buf);
950        buf.extend_from_slice(&cksum.to_le_bytes());
951        buf
952    }
953
954    #[test]
955    fn paged_prefix_roundtrip_and_bitmap() {
956        let ctx = ctx8();
957        // 11 pages => 2-byte bitmap. Initialize pages 0, 7, 8, 10.
958        let mut init = vec![false; 11];
959        for &p in &[0usize, 7, 8, 10] {
960            init[p] = true;
961        }
962        let buf = build_paged_prefix(&ctx, FA_CLIENT_CHUNK, 0xABCD, 11, &init);
963
964        let prefix = FixedArrayPagedPrefix::decode(&buf, &ctx, 11).unwrap();
965        assert_eq!(prefix.client_id, FA_CLIENT_CHUNK);
966        assert_eq!(prefix.header_addr, 0xABCD);
967        assert_eq!(prefix.prefix_size, buf.len());
968        // signature(4)+version(1)+client(1)+addr(8)+bitmap(2)+cksum(4) = 20
969        assert_eq!(prefix.prefix_size, 20);
970        for (p, &expected) in init.iter().enumerate() {
971            assert_eq!(prefix.page_initialized(p), expected, "page {p}");
972        }
973    }
974
975    #[test]
976    fn paged_prefix_bad_checksum() {
977        let ctx = ctx8();
978        let mut buf = build_paged_prefix(&ctx, FA_CLIENT_CHUNK, 0x1000, 3, &[true, true, true]);
979        buf[6] ^= 0xFF;
980        let err = FixedArrayPagedPrefix::decode(&buf, &ctx, 3).unwrap_err();
981        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
982    }
983
984    #[test]
985    fn unfiltered_page_roundtrip() {
986        let ctx = ctx8();
987        let addrs = [0x100u64, UNDEF_ADDR, 0x300, 0x400];
988        let page = build_unfiltered_page(&ctx, &addrs);
989        let decoded = decode_unfiltered_page(&page, &ctx, 4).unwrap();
990        assert_eq!(decoded, addrs);
991    }
992
993    #[test]
994    fn unfiltered_page_bad_checksum() {
995        let ctx = ctx8();
996        let mut page = build_unfiltered_page(&ctx, &[0x100u64, 0x200]);
997        page[0] ^= 0xFF;
998        let err = decode_unfiltered_page(&page, &ctx, 2).unwrap_err();
999        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
1000    }
1001
1002    #[test]
1003    fn filtered_page_roundtrip() {
1004        let ctx = ctx8();
1005        let csl = 4;
1006        let elems = vec![
1007            FixedArrayFilteredChunkElement {
1008                address: 0x2000,
1009                chunk_size: 321,
1010                filter_mask: 0,
1011            },
1012            FixedArrayFilteredChunkElement {
1013                address: 0x3000,
1014                chunk_size: 654,
1015                filter_mask: 2,
1016            },
1017        ];
1018        let page = build_filtered_page(&ctx, csl, &elems);
1019        let decoded = decode_filtered_page(&page, &ctx, 2, csl).unwrap();
1020        assert_eq!(decoded, elems);
1021    }
1022
1023    #[test]
1024    fn page_too_short_errors() {
1025        let ctx = ctx8();
1026        let page = build_unfiltered_page(&ctx, &[0x100u64, 0x200]);
1027        // Ask for more elements than the buffer holds.
1028        let err = decode_unfiltered_page(&page, &ctx, 4).unwrap_err();
1029        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1030    }
1031
1032    #[test]
1033    fn paged_prefix_encode_decode_roundtrip() {
1034        let ctx = ctx8();
1035        // 17 pages => 3-byte bitmap. Initialize pages 0, 8, 15, 16.
1036        let npages = 17usize;
1037        let mut bitmap = vec![0u8; npages.div_ceil(8)];
1038        for &p in &[0usize, 8, 15, 16] {
1039            bitmap[p / 8] |= 0x80u8 >> (p % 8);
1040        }
1041        let prefix = FixedArrayPagedPrefix {
1042            client_id: FA_CLIENT_CHUNK,
1043            header_addr: 0xDEAD_BEEF,
1044            page_init_bitmap: bitmap.clone(),
1045            prefix_size: 4 + 1 + 1 + 8 + bitmap.len() + 4,
1046        };
1047        let encoded = prefix.encode(&ctx);
1048        assert_eq!(encoded.len(), prefix.prefix_size);
1049        assert_eq!(&encoded[..4], b"FADB");
1050
1051        let decoded = FixedArrayPagedPrefix::decode(&encoded, &ctx, npages as u64).unwrap();
1052        assert_eq!(decoded.client_id, FA_CLIENT_CHUNK);
1053        assert_eq!(decoded.header_addr, 0xDEAD_BEEF);
1054        assert_eq!(decoded.page_init_bitmap, bitmap);
1055        assert_eq!(decoded.prefix_size, prefix.prefix_size);
1056        for p in 0..npages {
1057            let expected = [0usize, 8, 15, 16].contains(&p);
1058            assert_eq!(decoded.page_initialized(p), expected, "page {p}");
1059        }
1060    }
1061
1062    #[test]
1063    fn unfiltered_page_encode_decode_roundtrip() {
1064        let ctx = ctx8();
1065        let addrs = [0x1000u64, 0x2000, UNDEF_ADDR, 0x4000];
1066        let encoded = encode_unfiltered_page(&addrs, &ctx);
1067        assert_eq!(encoded.len(), addrs.len() * 8 + 4);
1068        let decoded = decode_unfiltered_page(&encoded, &ctx, addrs.len()).unwrap();
1069        assert_eq!(decoded, addrs);
1070    }
1071
1072    #[test]
1073    fn filtered_page_encode_decode_roundtrip() {
1074        let ctx = ctx8();
1075        let csl = 4;
1076        let elems = vec![
1077            FixedArrayFilteredChunkElement {
1078                address: 0x5000,
1079                chunk_size: 123,
1080                filter_mask: 0,
1081            },
1082            FixedArrayFilteredChunkElement {
1083                address: 0x6000,
1084                chunk_size: 456,
1085                filter_mask: 1,
1086            },
1087        ];
1088        let encoded = encode_filtered_page(&elems, &ctx, csl);
1089        assert_eq!(encoded.len(), elems.len() * (8 + csl + 4) + 4);
1090        let decoded = decode_filtered_page(&encoded, &ctx, elems.len(), csl).unwrap();
1091        assert_eq!(decoded, elems);
1092    }
1093
1094    #[test]
1095    fn paged_prefix_encode_matches_test_builder() {
1096        let ctx = ctx8();
1097        let npages = 11usize;
1098        let mut init = vec![false; npages];
1099        for &p in &[0usize, 7, 8, 10] {
1100            init[p] = true;
1101        }
1102        let from_builder = build_paged_prefix(&ctx, FA_CLIENT_CHUNK, 0xABCD, npages, &init);
1103
1104        let mut bitmap = vec![0u8; npages.div_ceil(8)];
1105        for (p, &on) in init.iter().enumerate() {
1106            if on {
1107                bitmap[p / 8] |= 0x80u8 >> (p % 8);
1108            }
1109        }
1110        let prefix = FixedArrayPagedPrefix {
1111            client_id: FA_CLIENT_CHUNK,
1112            header_addr: 0xABCD,
1113            page_init_bitmap: bitmap.clone(),
1114            prefix_size: 4 + 1 + 1 + 8 + bitmap.len() + 4,
1115        };
1116        assert_eq!(prefix.encode(&ctx), from_builder);
1117    }
1118}