Skip to main content

rustyhdf5_format/
chunked_write.rs

1//! Chunked dataset writing: chunk splitting, compression, index building.
2
3#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6#[cfg(not(feature = "std"))]
7use alloc::{vec, vec::Vec};
8
9use crate::checksum::jenkins_lookup3;
10use crate::chunk_cache::{CACHE_LINE_SIZE, align_to_cache_line};
11use crate::error::FormatError;
12use crate::filter_pipeline::{
13    FilterDescription, FilterPipeline, FILTER_DEFLATE, FILTER_FLETCHER32, FILTER_SHUFFLE,
14};
15use crate::filters::compress_chunk;
16
17/// Round a file offset up to the next cache-line boundary.
18///
19/// This ensures chunk data starts at an address that is a multiple of the
20/// architecture's cache line size, enabling aligned loads in SIMD paths.
21#[inline]
22pub fn align_chunk_offset(offset: u64) -> u64 {
23    let align = CACHE_LINE_SIZE as u64;
24    (offset + align - 1) & !(align - 1)
25}
26
27/// Options for chunked dataset creation.
28#[derive(Debug, Clone, Default)]
29pub struct ChunkOptions {
30    /// Chunk dimensions (one per dataset dimension).
31    pub chunk_dims: Option<Vec<u64>>,
32    /// Deflate compression level (0-9), None = no deflate.
33    pub deflate_level: Option<u32>,
34    /// Whether to apply shuffle filter before compression.
35    pub shuffle: bool,
36    /// Whether to apply fletcher32 checksum.
37    pub fletcher32: bool,
38}
39
40impl ChunkOptions {
41    /// Whether any chunking option is enabled.
42    pub fn is_chunked(&self) -> bool {
43        self.chunk_dims.is_some()
44            || self.deflate_level.is_some()
45            || self.shuffle
46            || self.fletcher32
47    }
48
49    /// Build a FilterPipeline from the options.
50    pub fn build_pipeline(&self, element_size: u32) -> Option<FilterPipeline> {
51        let mut filters = Vec::new();
52
53        if self.shuffle {
54            filters.push(FilterDescription {
55                filter_id: FILTER_SHUFFLE,
56                name: None,
57                flags: 0,
58                client_data: vec![element_size],
59            });
60        }
61
62        if let Some(level) = self.deflate_level {
63            filters.push(FilterDescription {
64                filter_id: FILTER_DEFLATE,
65                name: None,
66                flags: 0,
67                client_data: vec![level],
68            });
69        }
70
71        if self.fletcher32 {
72            filters.push(FilterDescription {
73                filter_id: FILTER_FLETCHER32,
74                name: None,
75                flags: 0,
76                client_data: vec![],  
77            });
78        }
79
80        // Note: h5py sets flags=0x0001 (optional) on filters, but this is not required
81        // for read compatibility.
82
83        if filters.is_empty() {
84            None
85        } else {
86            Some(FilterPipeline {
87                version: 2,
88                filters,
89            })
90        }
91    }
92
93    /// Determine chunk dimensions, using user-specified or auto-computing.
94    pub fn resolve_chunk_dims(&self, shape: &[u64]) -> Vec<u64> {
95        if let Some(ref dims) = self.chunk_dims {
96            dims.clone()
97        } else {
98            // Auto chunk: use the full dataset shape (single chunk)
99            shape.to_vec()
100        }
101    }
102}
103
104/// A chunk that has been written to the file buffer.
105#[derive(Debug, Clone)]
106pub struct WrittenChunk {
107    /// Address within the file where chunk data starts.
108    pub address: u64,
109    /// Size of the (possibly compressed) chunk data in bytes.
110    pub compressed_size: u64,
111    /// Original uncompressed size in bytes.
112    pub raw_size: u64,
113    /// Filter mask (0 = all filters applied).
114    pub filter_mask: u32,
115}
116
117/// Result of building a chunked dataset.
118pub struct ChunkedDataResult {
119    /// Raw bytes containing all chunk data + index structures.
120    pub data_bytes: Vec<u8>,
121    /// The DataLayout v4 message bytes.
122    pub layout_message: Vec<u8>,
123    /// The FilterPipeline message bytes, if any.
124    pub pipeline_message: Option<Vec<u8>>,
125}
126
127/// Split raw data into chunk-sized pieces based on shape and chunk dimensions.
128/// Returns a Vec of (chunk_offset_per_dim, chunk_raw_bytes).
129pub fn split_into_chunks(
130    raw_data: &[u8],
131    shape: &[u64],
132    chunk_dims: &[u64],
133    element_size: usize,
134) -> Vec<(Vec<u64>, Vec<u8>)> {
135    let rank = shape.len();
136    if rank == 0 {
137        return vec![(vec![], raw_data.to_vec())];
138    }
139
140    // Compute number of chunks per dimension
141    let mut num_chunks_per_dim = Vec::with_capacity(rank);
142    for d in 0..rank {
143        num_chunks_per_dim.push(shape[d].div_ceil(chunk_dims[d]));
144    }
145    let total_chunks: u64 = num_chunks_per_dim.iter().product();
146
147    // Dataset strides (row-major)
148    let mut ds_strides = vec![1usize; rank];
149    for i in (0..rank.saturating_sub(1)).rev() {
150        ds_strides[i] = ds_strides[i + 1] * shape[i + 1] as usize;
151    }
152
153    // Chunk strides
154    let mut chunk_strides = vec![1usize; rank];
155    for i in (0..rank.saturating_sub(1)).rev() {
156        chunk_strides[i] = chunk_strides[i + 1] * chunk_dims[i + 1] as usize;
157    }
158
159    let chunk_total_elements: usize = chunk_dims.iter().map(|&d| d as usize).product();
160
161    let mut result = Vec::with_capacity(total_chunks as usize);
162
163    for linear_idx in 0..total_chunks {
164        // Convert linear index to chunk grid coordinates
165        let mut chunk_grid_coords = vec![0u64; rank];
166        let mut remaining = linear_idx;
167        for d in (0..rank).rev() {
168            chunk_grid_coords[d] = remaining % num_chunks_per_dim[d];
169            remaining /= num_chunks_per_dim[d];
170        }
171
172        // Chunk offset in dataset space
173        let offsets: Vec<u64> = (0..rank)
174            .map(|d| chunk_grid_coords[d] * chunk_dims[d])
175            .collect();
176
177        // Extract chunk data
178        let mut chunk_bytes = vec![0u8; chunk_total_elements * element_size];
179
180        for flat_idx in 0..chunk_total_elements {
181            let mut remaining_idx = flat_idx;
182            let mut ds_flat = 0usize;
183            let mut out_of_bounds = false;
184
185            for d in 0..rank {
186                let coord_in_chunk = remaining_idx / chunk_strides[d];
187                remaining_idx %= chunk_strides[d];
188
189                let global_coord = offsets[d] as usize + coord_in_chunk;
190                if global_coord >= shape[d] as usize {
191                    out_of_bounds = true;
192                    break;
193                }
194                ds_flat += global_coord * ds_strides[d];
195            }
196
197            if out_of_bounds {
198                // Zero-filled (already initialized)
199                continue;
200            }
201
202            let src_start = ds_flat * element_size;
203            let dst_start = flat_idx * element_size;
204
205            if src_start + element_size <= raw_data.len() {
206                chunk_bytes[dst_start..dst_start + element_size]
207                    .copy_from_slice(&raw_data[src_start..src_start + element_size]);
208            }
209        }
210
211        result.push((offsets, chunk_bytes));
212    }
213
214    result
215}
216
217/// Build the complete chunked dataset blob (chunk data + index) and return
218/// layout/pipeline messages. `base_address` is where the blob will be placed in the file.
219/// Serialize a v4 single chunk layout message (public for OH size estimation).
220pub fn serialize_v4_single_chunk_pub(
221    chunk_dims: &[u32],
222    chunk_address: u64,
223    filtered_size: Option<u64>,
224    filter_mask: Option<u32>,
225    offset_size: u8,
226    element_size: u32,
227) -> Vec<u8> {
228    serialize_v4_single_chunk(
229        chunk_dims, chunk_address, filtered_size, filter_mask, offset_size, element_size,
230    )
231}
232
233/// Serialize a v4 single chunk layout message.
234fn serialize_v4_single_chunk(
235    chunk_dims: &[u32],
236    chunk_address: u64,
237    filtered_size: Option<u64>,
238    filter_mask: Option<u32>,
239    offset_size: u8,
240    element_size: u32,
241) -> Vec<u8> {
242    let mut buf = Vec::new();
243    buf.push(4); // version
244    buf.push(2); // class = chunked
245
246    // flags: bit 0 = unknown meaning in some files, bit 1 = filters for single chunk
247    let flags: u8 = if filtered_size.is_some() { 0x02 } else { 0x00 };
248    buf.push(flags);
249
250    // dimensionality = rank + 1 (chunk dims + element size dim)
251    let ndims = chunk_dims.len() as u8 + 1;
252    buf.push(ndims);
253
254    // dim_size_encoded_length: how many bytes per dimension
255    // We need to figure out the minimum encoding width
256    let max_dim = chunk_dims
257        .iter()
258        .map(|&d| d as u64)
259        .chain(core::iter::once(element_size as u64))
260        .max()
261        .unwrap_or(1);
262    let dim_encoded_len: u8 = if max_dim <= 0xFF {
263        1
264    } else if max_dim <= 0xFFFF {
265        2
266    } else {
267        4
268    };
269    buf.push(dim_encoded_len);
270
271    // dimension sizes (chunk dims + element size)
272    for &d in chunk_dims {
273        match dim_encoded_len {
274            1 => buf.push(d as u8),
275            2 => buf.extend_from_slice(&(d as u16).to_le_bytes()),
276            4 => buf.extend_from_slice(&d.to_le_bytes()),
277            _ => {}
278        }
279    }
280    // Element size dimension
281    match dim_encoded_len {
282        1 => buf.push(element_size as u8),
283        2 => buf.extend_from_slice(&(element_size as u16).to_le_bytes()),
284        4 => buf.extend_from_slice(&element_size.to_le_bytes()),
285        _ => {}
286    }
287
288    // chunk index type = 1 (single chunk)
289    buf.push(1);
290
291    // Index-specific fields
292    if let (Some(fs), Some(fm)) = (filtered_size, filter_mask) {
293        // filtered_size (length_size bytes)
294        buf.extend_from_slice(&fs.to_le_bytes()); // 8 bytes for length_size=8
295        buf.extend_from_slice(&fm.to_le_bytes()); // 4 bytes
296    }
297
298    // chunk address
299    match offset_size {
300        4 => buf.extend_from_slice(&(chunk_address as u32).to_le_bytes()),
301        8 => buf.extend_from_slice(&chunk_address.to_le_bytes()),
302        _ => {}
303    }
304
305    buf
306}
307
308/// Serialize a v4 Fixed Array layout message.
309fn serialize_v4_fixed_array(
310    chunk_dims: &[u32],
311    fixed_array_address: u64,
312    offset_size: u8,
313    element_size: u32,
314    max_bits: u8,
315) -> Vec<u8> {
316    let mut buf = Vec::new();
317    buf.push(4); // version
318    buf.push(2); // class = chunked
319
320    let flags: u8 = 0x00;
321    buf.push(flags);
322
323    let ndims = chunk_dims.len() as u8 + 1;
324    buf.push(ndims);
325
326    let max_dim = chunk_dims
327        .iter()
328        .map(|&d| d as u64)
329        .chain(core::iter::once(element_size as u64))
330        .max()
331        .unwrap_or(1);
332    let dim_encoded_len: u8 = if max_dim <= 0xFF {
333        1
334    } else if max_dim <= 0xFFFF {
335        2
336    } else {
337        4
338    };
339    buf.push(dim_encoded_len);
340
341    for &d in chunk_dims {
342        match dim_encoded_len {
343            1 => buf.push(d as u8),
344            2 => buf.extend_from_slice(&(d as u16).to_le_bytes()),
345            4 => buf.extend_from_slice(&d.to_le_bytes()),
346            _ => {}
347        }
348    }
349    match dim_encoded_len {
350        1 => buf.push(element_size as u8),
351        2 => buf.extend_from_slice(&(element_size as u16).to_le_bytes()),
352        4 => buf.extend_from_slice(&element_size.to_le_bytes()),
353        _ => {}
354    }
355
356    // chunk index type = 3 (Fixed Array)
357    buf.push(3);
358
359    // max_dblk_page_nelmts_bits — must match FAHD max_nelmts_bits
360    buf.push(max_bits);
361
362    // Fixed Array header address
363    match offset_size {
364        4 => buf.extend_from_slice(&(fixed_array_address as u32).to_le_bytes()),
365        8 => buf.extend_from_slice(&fixed_array_address.to_le_bytes()),
366        _ => {}
367    }
368
369    buf
370}
371
372/// Build a complete Fixed Array at a known absolute address.
373pub fn build_fixed_array_at(
374    chunks: &[WrittenChunk],
375    offset_size: u8,
376    length_size: u8,
377    has_filters: bool,
378    fa_base_address: u64,
379) -> Vec<u8> {
380    let os = offset_size as usize;
381    let num_elements = chunks.len();
382
383    // For filtered chunks, compute chunk_size encoding width.
384    // Must match the HDF5 C library's H5D_FARRAY_FILT_COMPUTE_CHUNK_SIZE_LEN macro:
385    //   chunk_size_len = 1 + ((H5VM_log2_gen(chunk.size) + 8) / 8)
386    // where chunk.size is the unfiltered chunk size in bytes (product of all chunk dims).
387    let chunk_size_bytes: usize = if has_filters {
388        let max_raw = chunks.iter().map(|c| c.raw_size).max().unwrap_or(1);
389        let log2_val = if max_raw <= 1 { 0 } else { 63 - max_raw.leading_zeros() };
390        let len = 1 + ((log2_val + 8) / 8) as usize;
391        len.min(8)
392    } else {
393        0
394    };
395
396    let elem_size = if has_filters {
397        os + chunk_size_bytes + 4
398    } else {
399        os
400    };
401
402    let client_id: u8 = if has_filters { 1 } else { 0 };
403
404    // FAHD total size
405    let nelmts_field_size = length_size as usize;
406    let fahd_total_size = 4 + 1 + 1 + 1 + 1 + nelmts_field_size + os + 4;
407    let fadb_address = fa_base_address + fahd_total_size as u64;
408
409    // Build FAHD
410    let mut fahd = Vec::with_capacity(fahd_total_size);
411    fahd.extend_from_slice(b"FAHD");
412    fahd.push(0); // version
413    fahd.push(client_id);
414    fahd.push(elem_size as u8);
415
416    // max_nelmts_bits: use 10 as default (page_size = 1024), matching h5py convention
417    let max_bits: u8 = 10;
418    fahd.push(max_bits);
419
420    match length_size {
421        4 => fahd.extend_from_slice(&(num_elements as u32).to_le_bytes()),
422        8 => fahd.extend_from_slice(&(num_elements as u64).to_le_bytes()),
423        _ => fahd.extend_from_slice(&(num_elements as u64).to_le_bytes()),
424    }
425
426    match offset_size {
427        4 => fahd.extend_from_slice(&(fadb_address as u32).to_le_bytes()),
428        8 => fahd.extend_from_slice(&fadb_address.to_le_bytes()),
429        _ => fahd.extend_from_slice(&fadb_address.to_le_bytes()),
430    }
431
432    // Checksum
433    let checksum = jenkins_lookup3(&fahd);
434    fahd.extend_from_slice(&checksum.to_le_bytes());
435
436    assert_eq!(fahd.len(), fahd_total_size);
437
438    // Build FADB
439    let mut fadb = Vec::new();
440    fadb.extend_from_slice(b"FADB");
441    fadb.push(0); // version
442    fadb.push(client_id);
443
444    // header address
445    match offset_size {
446        4 => fadb.extend_from_slice(&(fa_base_address as u32).to_le_bytes()),
447        8 => fadb.extend_from_slice(&fa_base_address.to_le_bytes()),
448        _ => fadb.extend_from_slice(&fa_base_address.to_le_bytes()),
449    }
450
451    // Element data
452    for chunk in chunks {
453        match offset_size {
454            4 => fadb.extend_from_slice(&(chunk.address as u32).to_le_bytes()),
455            8 => fadb.extend_from_slice(&chunk.address.to_le_bytes()),
456            _ => fadb.extend_from_slice(&chunk.address.to_le_bytes()),
457        }
458        if has_filters {
459            // Write compressed size using chunk_size_bytes (variable width)
460            let cs_bytes = chunk.compressed_size.to_le_bytes();
461            fadb.extend_from_slice(&cs_bytes[..chunk_size_bytes]);
462            fadb.extend_from_slice(&chunk.filter_mask.to_le_bytes());
463        }
464    }
465
466    // FADB checksum
467    let fadb_checksum = jenkins_lookup3(&fadb);
468    fadb.extend_from_slice(&fadb_checksum.to_le_bytes());
469
470    let mut combined = fahd;
471    combined.extend_from_slice(&fadb);
472    combined
473}
474
475/// Serialize a v4 Extensible Array layout message.
476fn serialize_v4_extensible_array(
477    chunk_dims: &[u32],
478    ea_address: u64,
479    offset_size: u8,
480    element_size: u32,
481) -> Vec<u8> {
482    let mut buf = Vec::new();
483    buf.push(4); // version
484    buf.push(2); // class = chunked
485    buf.push(0x00); // flags
486
487    let ndims = chunk_dims.len() as u8 + 1;
488    buf.push(ndims);
489
490    let max_dim = chunk_dims
491        .iter()
492        .map(|&d| d as u64)
493        .chain(core::iter::once(element_size as u64))
494        .max()
495        .unwrap_or(1);
496    let dim_encoded_len: u8 = if max_dim <= 0xFF {
497        1
498    } else if max_dim <= 0xFFFF {
499        2
500    } else {
501        4
502    };
503    buf.push(dim_encoded_len);
504
505    for &d in chunk_dims {
506        match dim_encoded_len {
507            1 => buf.push(d as u8),
508            2 => buf.extend_from_slice(&(d as u16).to_le_bytes()),
509            4 => buf.extend_from_slice(&d.to_le_bytes()),
510            _ => {}
511        }
512    }
513    match dim_encoded_len {
514        1 => buf.push(element_size as u8),
515        2 => buf.extend_from_slice(&(element_size as u16).to_le_bytes()),
516        4 => buf.extend_from_slice(&element_size.to_le_bytes()),
517        _ => {}
518    }
519
520    // chunk index type = 4 (Extensible Array)
521    buf.push(4);
522
523    // EA creation parameters (must match AEHD and HDF5 C library defaults)
524    buf.push(32); // max_nelmts_bits
525    buf.push(4); // idx_blk_elmts
526    buf.push(4); // super_blk_min_data_ptrs
527    buf.push(16); // data_blk_min_elmts
528    buf.push(10); // max_dblk_page_nelmts_bits
529
530    // EA header address
531    match offset_size {
532        4 => buf.extend_from_slice(&(ea_address as u32).to_le_bytes()),
533        8 => buf.extend_from_slice(&ea_address.to_le_bytes()),
534        _ => {}
535    }
536
537    buf
538}
539
540/// Build a complete Extensible Array at a known absolute address.
541///
542/// For simplicity, we put all elements inline in the index block when the
543/// number of chunks is small (up to idx_blk_elmts), otherwise use inline +
544/// direct data blocks.
545pub fn build_extensible_array_at(
546    chunks: &[WrittenChunk],
547    offset_size: u8,
548    length_size: u8,
549    has_filters: bool,
550    ea_base_address: u64,
551) -> Vec<u8> {
552    let os = offset_size as usize;
553    let num_elements = chunks.len();
554
555    // Compute element encoding size (same logic as Fixed Array)
556    let chunk_size_bytes: usize = if has_filters {
557        let max_raw = chunks.iter().map(|c| c.raw_size).max().unwrap_or(1);
558        let log2_val = if max_raw <= 1 {
559            0
560        } else {
561            63 - max_raw.leading_zeros()
562        };
563        let len = 1 + ((log2_val + 8) / 8) as usize;
564        len.min(8)
565    } else {
566        0
567    };
568
569    let elem_size = if has_filters {
570        os + chunk_size_bytes + 4
571    } else {
572        os
573    };
574
575    let client_id: u8 = if has_filters { 1 } else { 0 };
576
577    // EA creation parameters — must match HDF5 C library defaults exactly
578    let max_nelmts_bits: u8 = 32;
579    let idx_blk_elmts: u8 = 4;
580    let min_dblk_nelmts: u8 = 16;
581    let super_blk_min_nelmts: u8 = 4;
582    let max_dblk_nelmts_bits: u8 = 10;
583
584    // EAHD size: fixed(12) + 6 stats(6*length_size) + addr(offset_size) + checksum(4)
585    let aehd_size = 4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
586        + 6 * length_size as usize + os + 4;
587    let aeib_address = ea_base_address + aehd_size as u64;
588
589    // Determine how many elements go inline vs data blocks
590    let n_inline = (idx_blk_elmts as usize).min(num_elements);
591    let remaining_after_inline = num_elements.saturating_sub(n_inline);
592
593    // Compute super block layout per HDF5 spec:
594    // nsblks = floor(log2((2^max_nelmts_bits - idx_blk_elmts) / data_blk_min_elmts)) + 1
595    // For each super block i:
596    //   ndblks_in_sblk = 2^floor(i/2)
597    //   dblk_nelmts = data_blk_min_elmts * 2^ceil(i/2)
598    // Super blocks 0..sup_blk_min_data_ptrs-1 have their data block addrs in EAIB directly.
599    // Super blocks sup_blk_min_data_ptrs..nsblks-1 get super block addresses.
600    let sblk_min = super_blk_min_nelmts as usize; // sup_blk_min_data_ptrs
601    // nsblks = log2(2^max_nelmts_bits / data_blk_min_elmts) + 1
602    //        = max_nelmts_bits - log2(data_blk_min_elmts) + 1
603    let log2_dblk_min = if min_dblk_nelmts <= 1 { 0 } else { (min_dblk_nelmts as u32).trailing_zeros() as usize };
604    let nsblks = (max_nelmts_bits as usize).saturating_sub(log2_dblk_min) + 1;
605
606    // Direct data block addresses (from super blocks 0..sblk_min-1)
607    let mut dblk_sizes: Vec<usize> = Vec::new();
608    for sblk_idx in 0..sblk_min.min(nsblks) {
609        let ndblks = 1usize << (sblk_idx / 2);
610        let dblk_nelmts = (min_dblk_nelmts as usize) * (1 << sblk_idx.div_ceil(2));
611        for _ in 0..ndblks {
612            dblk_sizes.push(dblk_nelmts);
613        }
614    }
615    let n_direct_dblks = dblk_sizes.len();
616
617    // Super block addresses (for super blocks sblk_min..nsblks-1)
618    let n_sblk_addrs = nsblks.saturating_sub(sblk_min);
619
620    // EAIB size: header + inline elements + direct dblk addresses + sblk addresses + checksum
621    let aeib_size = 4 + 1 + 1 + os // sig+ver+client+hdr_addr
622        + idx_blk_elmts as usize * elem_size // inline elements (always all slots)
623        + n_direct_dblks * os // direct data block addresses
624        + n_sblk_addrs * os // super block addresses
625        + 4; // checksum
626
627    // Build AEHD
628    let mut aehd = Vec::with_capacity(aehd_size);
629    aehd.extend_from_slice(b"EAHD");
630    aehd.push(0); // version
631    aehd.push(client_id);
632    aehd.push(elem_size as u8);
633    aehd.push(max_nelmts_bits);
634    aehd.push(idx_blk_elmts);
635    aehd.push(min_dblk_nelmts);
636    aehd.push(super_blk_min_nelmts);
637    aehd.push(max_dblk_nelmts_bits);
638
639    // 6 stats fields matching HDF5 C library:
640    // [0] = 0 (unknown/reserved), [1] = 0 (unknown/reserved),
641    // [2] = ndata_blks, [3] = data_blk_total_size (computed after building data blocks),
642    // [4] = nelmts, [5] = max_idx_set
643    // We'll compute ndata_blks and data_blk_size below, and fill with placeholder for now.
644    // Actually, we need to compute these before writing EAHD.
645    // Count data blocks that will have chunks:
646    let n_active_dblks: u64 = if remaining_after_inline > 0 {
647        let mut count = 0u64;
648        let mut ci = n_inline;
649        for &sz in &dblk_sizes {
650            if ci < num_elements {
651                count += 1;
652                ci += sz;
653            }
654        }
655        count
656    } else {
657        0
658    };
659    // Compute total data block size (we'll update after building, but estimate here)
660    // For now, compute the AEDB size per block: sig(4) + ver(1) + cid(1) + hdr_addr(os) + nelmts*elem_size + checksum(4)
661    let aedb_header_overhead = 4 + 1 + 1 + os + 4;
662    let data_blk_total_size: u64 = if remaining_after_inline > 0 {
663        let mut total = 0u64;
664        let mut ci = n_inline;
665        for &sz in &dblk_sizes {
666            if ci < num_elements {
667                total += (aedb_header_overhead + sz * elem_size) as u64;
668                ci += sz;
669            }
670        }
671        total
672    } else {
673        0
674    };
675    // max_idx_set: idx_blk_elmts + sum of data block sizes for active blocks
676    let max_idx_set: u64 = if remaining_after_inline > 0 {
677        let mut max_set = idx_blk_elmts as u64;
678        let mut ci = n_inline;
679        for &sz in &dblk_sizes {
680            if ci < num_elements {
681                max_set += sz as u64;
682                ci += sz;
683            }
684        }
685        max_set
686    } else {
687        idx_blk_elmts as u64
688    };
689
690    let write_length = |buf: &mut Vec<u8>, val: u64| {
691        match length_size {
692            4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
693            _ => buf.extend_from_slice(&val.to_le_bytes()),
694        }
695    };
696    let write_addr = |buf: &mut Vec<u8>, val: u64| {
697        match offset_size {
698            4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
699            _ => buf.extend_from_slice(&val.to_le_bytes()),
700        }
701    };
702
703    write_length(&mut aehd, 0); // stat[0]: reserved/unknown
704    write_length(&mut aehd, 0); // stat[1]: reserved/unknown
705    write_length(&mut aehd, n_active_dblks); // stat[2]: ndata_blks
706    write_length(&mut aehd, data_blk_total_size); // stat[3]: data_blk_total_size
707    write_length(&mut aehd, num_elements as u64); // stat[4]: nelmts
708    write_length(&mut aehd, max_idx_set); // stat[5]: max_idx_set
709
710    write_addr(&mut aehd, aeib_address);
711
712    let aehd_checksum = jenkins_lookup3(&aehd);
713    aehd.extend_from_slice(&aehd_checksum.to_le_bytes());
714    debug_assert_eq!(aehd.len(), aehd_size);
715
716    // Build AEIB
717    let mut aeib = Vec::with_capacity(aeib_size);
718    aeib.extend_from_slice(b"EAIB");
719    aeib.push(0); // version
720    aeib.push(client_id);
721
722    // header address
723    match offset_size {
724        4 => aeib.extend_from_slice(&(ea_base_address as u32).to_le_bytes()),
725        8 => aeib.extend_from_slice(&ea_base_address.to_le_bytes()),
726        _ => aeib.extend_from_slice(&ea_base_address.to_le_bytes()),
727    }
728
729    // Inline elements (always write idx_blk_elmts slots, fill unused with undefined)
730    #[allow(clippy::needless_range_loop)]
731    for i in 0..idx_blk_elmts as usize {
732        if i < n_inline {
733            write_chunk_element(&mut aeib, &chunks[i], offset_size, has_filters, chunk_size_bytes);
734        } else {
735            write_undefined_element(&mut aeib, offset_size, has_filters, chunk_size_bytes);
736        }
737    }
738
739    // Data block addresses + build data blocks
740    let mut data_blocks_buf = Vec::new();
741    let dblks_base = aeib_address + aeib_size as u64;
742    let mut dblk_cursor = dblks_base;
743    let mut chunk_idx = n_inline;
744
745    for &nelmts in &dblk_sizes {
746        if chunk_idx >= num_elements {
747            // No more chunks — write undefined address
748            match offset_size {
749                4 => aeib.extend_from_slice(&u32::MAX.to_le_bytes()),
750                8 => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
751                _ => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
752            }
753            continue;
754        }
755
756        // Write this data block's address
757        match offset_size {
758            4 => aeib.extend_from_slice(&(dblk_cursor as u32).to_le_bytes()),
759            8 => aeib.extend_from_slice(&dblk_cursor.to_le_bytes()),
760            _ => aeib.extend_from_slice(&dblk_cursor.to_le_bytes()),
761        }
762
763        // Build EADB
764        let mut aedb = Vec::new();
765        aedb.extend_from_slice(b"EADB");
766        aedb.push(0); // version
767        aedb.push(client_id);
768        match offset_size {
769            4 => aedb.extend_from_slice(&(ea_base_address as u32).to_le_bytes()),
770            8 => aedb.extend_from_slice(&ea_base_address.to_le_bytes()),
771            _ => aedb.extend_from_slice(&ea_base_address.to_le_bytes()),
772        }
773
774        // Block offset: encoded in ceil(max_nelmts_bits/8) bytes
775        // This is the EA-relative index of the first element in this data block
776        let blk_off_size = (max_nelmts_bits as usize).div_ceil(8);
777        let blk_off_val = (chunk_idx - n_inline) as u64;
778        aedb.extend_from_slice(&blk_off_val.to_le_bytes()[..blk_off_size]);
779
780        // Write elements (fill all nelmts slots, use undefined for empty)
781        for slot in 0..nelmts {
782            if chunk_idx + slot < num_elements {
783                write_chunk_element(
784                    &mut aedb,
785                    &chunks[chunk_idx + slot],
786                    offset_size,
787                    has_filters,
788                    chunk_size_bytes,
789                );
790            } else {
791                // Undefined slot
792                write_undefined_element(&mut aedb, offset_size, has_filters, chunk_size_bytes);
793            }
794        }
795
796        let aedb_checksum = jenkins_lookup3(&aedb);
797        aedb.extend_from_slice(&aedb_checksum.to_le_bytes());
798
799        dblk_cursor += aedb.len() as u64;
800        data_blocks_buf.extend_from_slice(&aedb);
801        chunk_idx += nelmts;
802    }
803
804    // Super block addresses (all undefined for now — we don't create super blocks)
805    for _ in 0..n_sblk_addrs {
806        match offset_size {
807            4 => aeib.extend_from_slice(&u32::MAX.to_le_bytes()),
808            8 => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
809            _ => aeib.extend_from_slice(&u64::MAX.to_le_bytes()),
810        }
811    }
812
813    // AEIB checksum
814    let aeib_checksum = jenkins_lookup3(&aeib);
815    aeib.extend_from_slice(&aeib_checksum.to_le_bytes());
816    debug_assert_eq!(aeib.len(), aeib_size);
817
818    let mut combined = aehd;
819    combined.extend_from_slice(&aeib);
820    combined.extend_from_slice(&data_blocks_buf);
821    combined
822}
823
824fn write_chunk_element(
825    buf: &mut Vec<u8>,
826    chunk: &WrittenChunk,
827    offset_size: u8,
828    has_filters: bool,
829    chunk_size_bytes: usize,
830) {
831    match offset_size {
832        4 => buf.extend_from_slice(&(chunk.address as u32).to_le_bytes()),
833        8 => buf.extend_from_slice(&chunk.address.to_le_bytes()),
834        _ => buf.extend_from_slice(&chunk.address.to_le_bytes()),
835    }
836    if has_filters {
837        let cs_bytes = chunk.compressed_size.to_le_bytes();
838        buf.extend_from_slice(&cs_bytes[..chunk_size_bytes]);
839        buf.extend_from_slice(&chunk.filter_mask.to_le_bytes());
840    }
841}
842
843fn write_undefined_element(
844    buf: &mut Vec<u8>,
845    offset_size: u8,
846    has_filters: bool,
847    chunk_size_bytes: usize,
848) {
849    let os = offset_size as usize;
850    buf.extend_from_slice(&vec![0xFF; os]);
851    if has_filters {
852        buf.extend_from_slice(&vec![0x00; chunk_size_bytes]);
853        buf.extend_from_slice(&0u32.to_le_bytes());
854    }
855}
856
857/// Build chunked data with absolute addresses.
858/// If `maxshape` has unlimited dims, uses Extensible Array index.
859pub fn build_chunked_data_at(
860    raw_data: &[u8],
861    shape: &[u64],
862    chunk_dims: &[u64],
863    element_size: usize,
864    options: &ChunkOptions,
865    base_address: u64,
866) -> Result<ChunkedDataResult, FormatError> {
867    build_chunked_data_at_ext(raw_data, shape, chunk_dims, element_size, options, base_address, None)
868}
869
870/// Build chunked data with absolute addresses and optional maxshape.
871pub fn build_chunked_data_at_ext(
872    raw_data: &[u8],
873    shape: &[u64],
874    chunk_dims: &[u64],
875    element_size: usize,
876    options: &ChunkOptions,
877    base_address: u64,
878    maxshape: Option<&[u64]>,
879) -> Result<ChunkedDataResult, FormatError> {
880    let pipeline = options.build_pipeline(element_size as u32);
881
882    let chunks = split_into_chunks(raw_data, shape, chunk_dims, element_size);
883    let num_chunks = chunks.len();
884    let has_filters = pipeline.is_some();
885
886    // Compress each chunk, padding to cache-line boundaries for aligned access
887    let mut data_buf = Vec::new();
888    let mut written_chunks = Vec::with_capacity(num_chunks);
889
890    for (_offsets, chunk_bytes) in &chunks {
891        let compressed = if let Some(ref pl) = pipeline {
892            compress_chunk(chunk_bytes, pl, element_size as u32)?
893        } else {
894            chunk_bytes.clone()
895        };
896
897        // Pad current position to cache-line boundary
898        let aligned_offset = align_to_cache_line(data_buf.len());
899        if aligned_offset > data_buf.len() {
900            data_buf.resize(aligned_offset, 0u8);
901        }
902
903        let address = base_address + data_buf.len() as u64;
904        let compressed_size = compressed.len() as u64;
905        let raw_size = chunk_bytes.len() as u64;
906
907        data_buf.extend_from_slice(&compressed);
908
909        written_chunks.push(WrittenChunk {
910            address,
911            compressed_size,
912            raw_size,
913            filter_mask: 0,
914        });
915    }
916
917    let chunk_dims_u32: Vec<u32> = chunk_dims.iter().map(|&d| d as u32).collect();
918    let offset_size: u8 = 8;
919    let length_size: u8 = 8;
920
921    // Determine if we should use Extensible Array (resizable datasets)
922    let use_extensible = maxshape.is_some_and(|ms| ms.contains(&u64::MAX));
923
924    // Pad before index structures so they are also cache-line aligned
925    let aligned_idx = align_to_cache_line(data_buf.len());
926    if aligned_idx > data_buf.len() {
927        data_buf.resize(aligned_idx, 0u8);
928    }
929
930    let layout_message = if use_extensible {
931        let ea_address = base_address + data_buf.len() as u64;
932
933        let ea_bytes = build_extensible_array_at(
934            &written_chunks,
935            offset_size,
936            length_size,
937            has_filters,
938            ea_address,
939        );
940        data_buf.extend_from_slice(&ea_bytes);
941
942        serialize_v4_extensible_array(
943            &chunk_dims_u32,
944            ea_address,
945            offset_size,
946            element_size as u32,
947        )
948    } else if num_chunks == 1 {
949        let chunk_addr = written_chunks[0].address;
950        let filtered_size = if has_filters {
951            Some(written_chunks[0].compressed_size)
952        } else {
953            None
954        };
955        let filter_mask = if has_filters { Some(0u32) } else { None };
956        serialize_v4_single_chunk(
957            &chunk_dims_u32,
958            chunk_addr,
959            filtered_size,
960            filter_mask,
961            offset_size,
962            element_size as u32,
963        )
964    } else {
965        let fa_address = base_address + data_buf.len() as u64;
966        let max_bits: u8 = 10;
967
968        let fa_bytes = build_fixed_array_at(
969            &written_chunks,
970            offset_size,
971            length_size,
972            has_filters,
973            fa_address,
974        );
975        data_buf.extend_from_slice(&fa_bytes);
976
977        serialize_v4_fixed_array(
978            &chunk_dims_u32,
979            fa_address,
980            offset_size,
981            element_size as u32,
982            max_bits,
983        )
984    };
985
986    let pipeline_message = pipeline.as_ref().map(|pl| pl.serialize());
987
988    Ok(ChunkedDataResult {
989        data_bytes: data_buf,
990        layout_message,
991        pipeline_message,
992    })
993}
994
995#[cfg(test)]
996mod tests {
997    use super::*;
998    use crate::chunked_read::read_chunked_data;
999    use crate::data_layout::DataLayout;
1000    use crate::dataspace::{Dataspace, DataspaceType};
1001    use crate::datatype::{Datatype, DatatypeByteOrder};
1002
1003    fn make_f64_type() -> Datatype {
1004        Datatype::FloatingPoint {
1005            size: 8,
1006            byte_order: DatatypeByteOrder::LittleEndian,
1007            bit_offset: 0,
1008            bit_precision: 64,
1009            exponent_location: 52,
1010            exponent_size: 11,
1011            mantissa_location: 0,
1012            mantissa_size: 52,
1013            exponent_bias: 1023,
1014        }
1015    }
1016
1017    fn f64_to_bytes(data: &[f64]) -> Vec<u8> {
1018        let mut b = Vec::with_capacity(data.len() * 8);
1019        for &v in data {
1020            b.extend_from_slice(&v.to_le_bytes());
1021        }
1022        b
1023    }
1024
1025    fn bytes_to_f64(data: &[u8]) -> Vec<f64> {
1026        data.chunks(8)
1027            .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
1028            .collect()
1029    }
1030
1031    /// Helper: build a chunked file blob and read it back using read_chunked_data
1032    fn roundtrip_chunked(
1033        values: &[f64],
1034        shape: &[u64],
1035        chunk_dims: &[u64],
1036        options: &ChunkOptions,
1037    ) -> Vec<f64> {
1038        let raw = f64_to_bytes(values);
1039        let base_address = 0x1000u64;
1040        let result =
1041            build_chunked_data_at(&raw, shape, chunk_dims, 8, options, base_address).unwrap();
1042
1043        // Build a fake file buffer
1044        let file_size = base_address as usize + result.data_bytes.len();
1045        let mut file_data = vec![0u8; file_size];
1046        file_data[base_address as usize..].copy_from_slice(&result.data_bytes);
1047
1048        // Parse layout
1049        let layout = DataLayout::parse(&result.layout_message, 8, 8).unwrap();
1050        let dataspace = Dataspace {
1051            space_type: DataspaceType::Simple,
1052            rank: shape.len() as u8,
1053            dimensions: shape.to_vec(),
1054            max_dimensions: None,
1055        };
1056        let datatype = make_f64_type();
1057
1058        // Parse pipeline if present
1059        let pipeline = result
1060            .pipeline_message
1061            .as_ref()
1062            .map(|pm| crate::filter_pipeline::FilterPipeline::parse(pm).unwrap());
1063
1064        let output = read_chunked_data(
1065            &file_data,
1066            &layout,
1067            &dataspace,
1068            &datatype,
1069            pipeline.as_ref(),
1070            8,
1071            8,
1072        )
1073        .unwrap();
1074
1075        bytes_to_f64(&output)
1076    }
1077
1078    #[test]
1079    fn split_1d_single_chunk() {
1080        let data = f64_to_bytes(&[1.0, 2.0, 3.0]);
1081        let result = split_into_chunks(&data, &[3], &[3], 8);
1082        assert_eq!(result.len(), 1);
1083        assert_eq!(result[0].0, vec![0]);
1084        assert_eq!(bytes_to_f64(&result[0].1), vec![1.0, 2.0, 3.0]);
1085    }
1086
1087    #[test]
1088    fn split_1d_multiple_chunks() {
1089        let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1090        let data = f64_to_bytes(&values);
1091        let result = split_into_chunks(&data, &[10], &[4], 8);
1092        assert_eq!(result.len(), 3); // ceil(10/4) = 3
1093        assert_eq!(result[0].0, vec![0]);
1094        assert_eq!(result[1].0, vec![4]);
1095        assert_eq!(result[2].0, vec![8]);
1096        assert_eq!(bytes_to_f64(&result[0].1), vec![0.0, 1.0, 2.0, 3.0]);
1097        assert_eq!(bytes_to_f64(&result[1].1), vec![4.0, 5.0, 6.0, 7.0]);
1098        // Last chunk: 2 valid + 2 padding zeros
1099        assert_eq!(bytes_to_f64(&result[2].1), vec![8.0, 9.0, 0.0, 0.0]);
1100    }
1101
1102    #[test]
1103    fn split_2d_chunks() {
1104        // 4x4 dataset, 2x2 chunks -> 4 chunks
1105        let values: Vec<f64> = (0..16).map(|i| i as f64).collect();
1106        let data = f64_to_bytes(&values);
1107        let result = split_into_chunks(&data, &[4, 4], &[2, 2], 8);
1108        assert_eq!(result.len(), 4);
1109        assert_eq!(result[0].0, vec![0, 0]);
1110        assert_eq!(result[1].0, vec![0, 2]);
1111        assert_eq!(result[2].0, vec![2, 0]);
1112        assert_eq!(result[3].0, vec![2, 2]);
1113        // chunk (0,0): elements [0,1,4,5]
1114        assert_eq!(bytes_to_f64(&result[0].1), vec![0.0, 1.0, 4.0, 5.0]);
1115        // chunk (0,2): elements [2,3,6,7]
1116        assert_eq!(bytes_to_f64(&result[1].1), vec![2.0, 3.0, 6.0, 7.0]);
1117    }
1118
1119    #[test]
1120    fn roundtrip_1d_single_chunk_no_compression() {
1121        let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1122        let options = ChunkOptions {
1123            chunk_dims: Some(vec![10]),
1124            ..Default::default()
1125        };
1126        let result = roundtrip_chunked(&values, &[10], &[10], &options);
1127        assert_eq!(result, values);
1128    }
1129
1130    #[cfg(feature = "deflate")]
1131    #[test]
1132    fn roundtrip_1d_single_chunk_deflate() {
1133        let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1134        let options = ChunkOptions {
1135            chunk_dims: Some(vec![100]),
1136            deflate_level: Some(6),
1137            ..Default::default()
1138        };
1139        let result = roundtrip_chunked(&values, &[100], &[100], &options);
1140        assert_eq!(result, values);
1141    }
1142
1143    #[test]
1144    fn roundtrip_1d_multi_chunk_no_compression() {
1145        let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1146        let options = ChunkOptions {
1147            chunk_dims: Some(vec![8]),
1148            ..Default::default()
1149        };
1150        let result = roundtrip_chunked(&values, &[20], &[8], &options);
1151        assert_eq!(result, values);
1152    }
1153
1154    #[cfg(feature = "deflate")]
1155    #[test]
1156    fn roundtrip_1d_multi_chunk_deflate() {
1157        let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1158        let options = ChunkOptions {
1159            chunk_dims: Some(vec![20]),
1160            deflate_level: Some(6),
1161            ..Default::default()
1162        };
1163        let result = roundtrip_chunked(&values, &[100], &[20], &options);
1164        assert_eq!(result, values);
1165    }
1166
1167    #[cfg(feature = "deflate")]
1168    #[test]
1169    fn roundtrip_1d_shuffle_deflate() {
1170        let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1171        let options = ChunkOptions {
1172            chunk_dims: Some(vec![50]),
1173            deflate_level: Some(6),
1174            shuffle: true,
1175            ..Default::default()
1176        };
1177        let result = roundtrip_chunked(&values, &[100], &[50], &options);
1178        assert_eq!(result, values);
1179    }
1180
1181    #[test]
1182    fn roundtrip_2d_chunks() {
1183        // 6x4 dataset, 3x2 chunks
1184        let values: Vec<f64> = (0..24).map(|i| i as f64).collect();
1185        let options = ChunkOptions {
1186            chunk_dims: Some(vec![3, 2]),
1187            ..Default::default()
1188        };
1189        let result = roundtrip_chunked(&values, &[6, 4], &[3, 2], &options);
1190        assert_eq!(result, values);
1191    }
1192
1193    #[test]
1194    fn align_chunk_offset_values() {
1195        use super::align_chunk_offset;
1196        use super::CACHE_LINE_SIZE;
1197        let cl = CACHE_LINE_SIZE as u64;
1198        assert_eq!(align_chunk_offset(0), 0);
1199        assert_eq!(align_chunk_offset(1), cl);
1200        assert_eq!(align_chunk_offset(cl), cl);
1201        assert_eq!(align_chunk_offset(cl + 1), cl * 2);
1202        assert_eq!(align_chunk_offset(cl * 10), cl * 10);
1203    }
1204
1205    #[test]
1206    fn chunk_addresses_are_cache_aligned() {
1207        use super::{CACHE_LINE_SIZE, align_chunk_offset};
1208        let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1209        let raw = f64_to_bytes(&values);
1210        let base_address = 0x1000u64;
1211        // Ensure base is aligned for this test
1212        let base_address = align_chunk_offset(base_address);
1213        let options = ChunkOptions {
1214            chunk_dims: Some(vec![20]),
1215            ..Default::default()
1216        };
1217        let result =
1218            build_chunked_data_at(&raw, &[100], &[20], 8, &options, base_address).unwrap();
1219
1220        // Parse layout to get chunk addresses (via roundtrip read)
1221        let file_size = base_address as usize + result.data_bytes.len();
1222        let mut file_data = vec![0u8; file_size];
1223        file_data[base_address as usize..].copy_from_slice(&result.data_bytes);
1224
1225        let layout = DataLayout::parse(&result.layout_message, 8, 8).unwrap();
1226        let dataspace = Dataspace {
1227            space_type: DataspaceType::Simple,
1228            rank: 1,
1229            dimensions: vec![100],
1230            max_dimensions: None,
1231        };
1232        let datatype = make_f64_type();
1233
1234        // Verify data roundtrips correctly
1235        let output = read_chunked_data(
1236            &file_data, &layout, &dataspace, &datatype, None, 8, 8,
1237        ).unwrap();
1238        assert_eq!(bytes_to_f64(&output), values);
1239    }
1240
1241    #[test]
1242    fn chunk_options_auto_dims() {
1243        let options = ChunkOptions {
1244            chunk_dims: None,
1245            deflate_level: Some(6),
1246            ..Default::default()
1247        };
1248        let dims = options.resolve_chunk_dims(&[100, 50]);
1249        assert_eq!(dims, vec![100, 50]);
1250    }
1251
1252    #[test]
1253    fn chunk_options_pipeline_deflate() {
1254        let options = ChunkOptions {
1255            deflate_level: Some(6),
1256            ..Default::default()
1257        };
1258        let pl = options.build_pipeline(8).unwrap();
1259        assert_eq!(pl.filters.len(), 1);
1260        assert_eq!(pl.filters[0].filter_id, FILTER_DEFLATE);
1261    }
1262
1263    #[test]
1264    fn chunk_options_pipeline_shuffle_deflate_fletcher32() {
1265        let options = ChunkOptions {
1266            deflate_level: Some(6),
1267            shuffle: true,
1268            fletcher32: true,
1269            ..Default::default()
1270        };
1271        let pl = options.build_pipeline(8).unwrap();
1272        assert_eq!(pl.filters.len(), 3);
1273        assert_eq!(pl.filters[0].filter_id, FILTER_SHUFFLE);
1274        assert_eq!(pl.filters[1].filter_id, FILTER_DEFLATE);
1275        assert_eq!(pl.filters[2].filter_id, FILTER_FLETCHER32);
1276    }
1277
1278    #[test]
1279    fn serialize_v4_single_chunk_no_filters_roundtrip() {
1280        let msg = serialize_v4_single_chunk(&[20], 0x1000, None, None, 8, 8);
1281        let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1282        match layout {
1283            DataLayout::Chunked {
1284                chunk_dimensions,
1285                btree_address,
1286                version,
1287                chunk_index_type,
1288                single_chunk_filtered_size,
1289                single_chunk_filter_mask,
1290            } => {
1291                assert_eq!(version, 4);
1292                assert_eq!(chunk_index_type, Some(1));
1293                assert_eq!(chunk_dimensions, vec![20, 8]);
1294                assert_eq!(btree_address, Some(0x1000));
1295                assert_eq!(single_chunk_filtered_size, None);
1296                assert_eq!(single_chunk_filter_mask, None);
1297            }
1298            _ => panic!("expected chunked layout"),
1299        }
1300    }
1301
1302    #[test]
1303    fn serialize_v4_single_chunk_with_filters_roundtrip() {
1304        let msg = serialize_v4_single_chunk(&[100], 0x2000, Some(500), Some(0), 8, 8);
1305        let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1306        match layout {
1307            DataLayout::Chunked {
1308                btree_address,
1309                single_chunk_filtered_size,
1310                single_chunk_filter_mask,
1311                ..
1312            } => {
1313                assert_eq!(btree_address, Some(0x2000));
1314                assert_eq!(single_chunk_filtered_size, Some(500));
1315                assert_eq!(single_chunk_filter_mask, Some(0));
1316            }
1317            _ => panic!("expected chunked layout"),
1318        }
1319    }
1320
1321    #[test]
1322    fn serialize_v4_fixed_array_roundtrip() {
1323        let msg = serialize_v4_fixed_array(&[20], 0x3000, 8, 8, 4);
1324        let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1325        match layout {
1326            DataLayout::Chunked {
1327                version,
1328                chunk_index_type,
1329                btree_address,
1330                chunk_dimensions,
1331                ..
1332            } => {
1333                assert_eq!(version, 4);
1334                assert_eq!(chunk_index_type, Some(3));
1335                assert_eq!(btree_address, Some(0x3000));
1336                assert_eq!(chunk_dimensions, vec![20, 8]);
1337            }
1338            _ => panic!("expected chunked layout"),
1339        }
1340    }
1341
1342    #[test]
1343    fn build_fixed_array_valid_structure() {
1344        let chunks = vec![
1345            WrittenChunk {
1346                address: 0x1000,
1347                compressed_size: 160,
1348                raw_size: 160,
1349                filter_mask: 0,
1350            },
1351            WrittenChunk {
1352                address: 0x10A0,
1353                compressed_size: 160,
1354                raw_size: 160,
1355                filter_mask: 0,
1356            },
1357        ];
1358        let fa = build_fixed_array_at(&chunks, 8, 8, false, 0x2000);
1359        // Should start with FAHD
1360        assert_eq!(&fa[0..4], b"FAHD");
1361        // FAHD size = 4+1+1+1+1+8+8+4 = 28
1362        // FADB starts at offset 28
1363        assert_eq!(&fa[28..32], b"FADB");
1364    }
1365
1366    // ---- Extensible Array tests ----
1367
1368    #[test]
1369    fn serialize_v4_extensible_array_roundtrip() {
1370        let msg = serialize_v4_extensible_array(&[10], 0x4000, 8, 8);
1371        let layout = DataLayout::parse(&msg, 8, 8).unwrap();
1372        match layout {
1373            DataLayout::Chunked {
1374                version,
1375                chunk_index_type,
1376                btree_address,
1377                chunk_dimensions,
1378                ..
1379            } => {
1380                assert_eq!(version, 4);
1381                assert_eq!(chunk_index_type, Some(4));
1382                assert_eq!(btree_address, Some(0x4000));
1383                assert_eq!(chunk_dimensions, vec![10, 8]);
1384            }
1385            _ => panic!("expected chunked layout"),
1386        }
1387    }
1388
1389    #[test]
1390    fn build_extensible_array_valid_structure() {
1391        let chunks = vec![
1392            WrittenChunk {
1393                address: 0x1000,
1394                compressed_size: 80,
1395                raw_size: 80,
1396                filter_mask: 0,
1397            },
1398            WrittenChunk {
1399                address: 0x1050,
1400                compressed_size: 80,
1401                raw_size: 80,
1402                filter_mask: 0,
1403            },
1404        ];
1405        let ea = build_extensible_array_at(&chunks, 8, 8, false, 0x2000);
1406        assert_eq!(&ea[0..4], b"EAHD");
1407        // Find EAIB after EAHD: 12 fixed + 6*8 stats + 8 addr + 4 checksum = 72
1408        let aehd_size = 4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * 8 + 8 + 4;
1409        assert_eq!(&ea[aehd_size..aehd_size + 4], b"EAIB");
1410    }
1411
1412    /// Helper: roundtrip with EA (maxshape)
1413    fn roundtrip_ea(
1414        values: &[f64],
1415        shape: &[u64],
1416        chunk_dims: &[u64],
1417        maxshape: &[u64],
1418    ) -> Vec<f64> {
1419        let raw = f64_to_bytes(values);
1420        let base_address = 0x1000u64;
1421        let options = ChunkOptions {
1422            chunk_dims: Some(chunk_dims.to_vec()),
1423            ..Default::default()
1424        };
1425        let result = build_chunked_data_at_ext(
1426            &raw, shape, chunk_dims, 8, &options, base_address, Some(maxshape),
1427        )
1428        .unwrap();
1429
1430        let file_size = base_address as usize + result.data_bytes.len();
1431        let mut file_data = vec![0u8; file_size];
1432        file_data[base_address as usize..].copy_from_slice(&result.data_bytes);
1433
1434        let layout = DataLayout::parse(&result.layout_message, 8, 8).unwrap();
1435        // Verify it uses EA index
1436        match &layout {
1437            DataLayout::Chunked { chunk_index_type, .. } => {
1438                assert_eq!(*chunk_index_type, Some(4), "expected EA index type");
1439            }
1440            _ => panic!("expected chunked layout"),
1441        }
1442
1443        let dataspace = Dataspace {
1444            space_type: DataspaceType::Simple,
1445            rank: shape.len() as u8,
1446            dimensions: shape.to_vec(),
1447            max_dimensions: Some(maxshape.to_vec()),
1448        };
1449        let datatype = make_f64_type();
1450
1451        let output = read_chunked_data(&file_data, &layout, &dataspace, &datatype, None, 8, 8)
1452            .unwrap();
1453
1454        bytes_to_f64(&output)
1455    }
1456
1457    #[test]
1458    fn ea_roundtrip_1d_inline_only() {
1459        let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1460        let result = roundtrip_ea(&values, &[10], &[10], &[u64::MAX]);
1461        assert_eq!(result, values);
1462    }
1463
1464    #[test]
1465    fn ea_roundtrip_1d_multi_chunks() {
1466        let values: Vec<f64> = (0..20).map(|i| i as f64).collect();
1467        let result = roundtrip_ea(&values, &[20], &[5], &[u64::MAX]);
1468        assert_eq!(result, values);
1469    }
1470
1471    #[test]
1472    fn ea_roundtrip_1d_many_chunks() {
1473        let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1474        let result = roundtrip_ea(&values, &[100], &[10], &[u64::MAX]);
1475        assert_eq!(result, values);
1476    }
1477
1478    // ---- h5py round-trip tests for chunked writes ----
1479
1480    #[cfg(feature = "std")]
1481    fn h5py_run(script: &str) -> String {
1482        let o = std::process::Command::new("python3").args(["-c", script]).output().expect("python3");
1483        if !o.status.success() { panic!("h5py: {}", String::from_utf8_lossy(&o.stderr)); }
1484        String::from_utf8(o.stdout).unwrap().trim().to_string()
1485    }
1486
1487    #[cfg(feature = "std")]
1488    #[test]
1489    fn h5py_reads_multiple_chunked_datasets() {
1490        use crate::file_writer::{FileWriter, AttrValue};
1491        let mut fw = FileWriter::new();
1492        let data1: Vec<f64> = (0..50).map(|i| i as f64).collect();
1493        let data2: Vec<f64> = (0..30).map(|i| (i * 10) as f64).collect();
1494        fw.create_dataset("a").with_f64_data(&data1).with_shape(&[50]).with_chunks(&[25]);
1495        fw.create_dataset("b").with_f64_data(&data2).with_shape(&[30]).with_chunks(&[10]);
1496        let bytes = fw.finish().unwrap();
1497        let path = std::env::temp_dir().join("rustyhdf5_chunked_multi.h5");
1498        std::fs::write(&path, &bytes).unwrap();
1499        let script = format!(
1500            "import h5py,json; f=h5py.File('{}','r'); print(json.dumps({{'a':f['a'][:].tolist(),'b':f['b'][:].tolist()}}))",
1501            path.display()
1502        );
1503        let v: serde_json::Value = serde_json::from_str(&h5py_run(&script)).unwrap();
1504        let va: Vec<f64> = serde_json::from_value(v["a"].clone()).unwrap();
1505        let vb: Vec<f64> = serde_json::from_value(v["b"].clone()).unwrap();
1506        assert_eq!(va, data1);
1507        assert_eq!(vb, data2);
1508    }
1509
1510    #[cfg(feature = "std")]
1511    #[test]
1512    fn h5py_reads_chunked_with_attrs() {
1513        use crate::file_writer::{FileWriter, AttrValue};
1514        let mut fw = FileWriter::new();
1515        let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
1516        fw.create_dataset("data").with_f64_data(&data).with_shape(&[50]).with_chunks(&[25])
1517            .set_attr("units", AttrValue::String("meters".to_string()));
1518        let bytes = fw.finish().unwrap();
1519        let path = std::env::temp_dir().join("rustyhdf5_chunked_attrs.h5");
1520        std::fs::write(&path, &bytes).unwrap();
1521        let script = format!(
1522            "import h5py,json; f=h5py.File('{}','r'); d=f['data']; print(json.dumps({{'values':d[:].tolist(),'units':d.attrs['units'].decode() if isinstance(d.attrs['units'],bytes) else str(d.attrs['units'])}}))",
1523            path.display()
1524        );
1525        let v: serde_json::Value = serde_json::from_str(&h5py_run(&script)).unwrap();
1526        let values: Vec<f64> = serde_json::from_value(v["values"].clone()).unwrap();
1527        assert_eq!(values, data);
1528        assert_eq!(v["units"], serde_json::json!("meters"));
1529    }
1530}