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