catwalk3d 2.0.0

Headless model thumbnails rendered entirely in Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
//! Scientific-container data adapters using unmodified upstream HDF5 and NetCDF APIs.
//!
//! Metadata, ordinary data layouts, filters, numeric types and CF processing
//! remain upstream-owned. Catwalk supplies only affected filtered-index reads,
//! bounded chunk assembly and the corresponding upstream array construction.

#[path = "hdf5_chunks.rs"]
/// Variable-width filtered chunk indexes.
mod chunks;

use crate::Result;
use hdf5_reader::{
    BytesStorage, Datatype, H5Type, Storage, StorageBuffer,
    filters::apply_pipeline_with_limit,
    io::Cursor,
    messages::{HdfMessage, layout::DataLayout},
    object_header::ObjectHeader,
    superblock::Superblock,
};
use ndarray::{ArrayD, IxDyn};
use netcdf_reader::{NcReadable, NcSliceInfo, NcSliceInfoElem};
use std::{ops::Deref, sync::Arc};

/// Maximum decoded array size, matching the shared scientific/Alembic byte ceiling.
const MAX_BYTES: usize = 512 * 1024 * 1024;
/// Maximum number of selected numeric samples.
const MAX_SAMPLES: usize = 16_777_216;

/// Upstream HDF5 metadata together with a shared immutable view of its source bytes.
pub(super) struct Hdf5File {
    /// Unmodified high-level metadata and ordinary data reader.
    inner: hdf5_reader::Hdf5File,
    /// File bytes shared with the upstream random-access storage.
    bytes: StorageBuffer,
    /// Public superblock metadata describing addresses and length widths.
    superblock: Superblock,
}

impl Deref for Hdf5File {
    type Target = hdf5_reader::Hdf5File;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl Hdf5File {
    /// Open bounded caller-owned bytes with the scientific reader's small chunk cache.
    ///
    /// # Errors
    /// Returns an error for invalid HDF5 metadata or address ranges.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
        Self::from_vec(bytes.to_vec())
    }

    /// Transfer an existing file buffer into upstream immutable storage without copying it.
    ///
    /// # Errors
    /// Returns an error for invalid HDF5 metadata or address ranges.
    pub fn from_vec(bytes: Vec<u8>) -> Result<Self> {
        Self::from_storage(Arc::new(BytesStorage::new(bytes)))
    }

    /// Share storage with another upstream container reader without duplicating file bytes.
    fn from_storage(storage: Arc<BytesStorage>) -> Result<Self> {
        let superblock = Superblock::parse_from_storage(storage.as_ref())?;
        let bytes = storage.read_range(0, usize::try_from(storage.len())?)?;
        let inner = hdf5_reader::Hdf5File::from_storage_with_options(
            storage,
            hdf5_reader::OpenOptions {
                chunk_cache_bytes: 8 * 1024 * 1024,
                chunk_cache_slots: 257,
                ..Default::default()
            },
        )?;
        Ok(Self {
            inner,
            bytes,
            superblock,
        })
    }

    /// Open an upstream dataset and attach the adapter's immutable source context.
    ///
    /// # Errors
    /// Returns upstream lookup and dataset-metadata errors.
    pub fn dataset(&self, path: &str) -> Result<Dataset> {
        Ok(self.adapt_dataset(self.inner.dataset(path)?))
    }

    /// Attach shared source context to a dataset returned through an upstream group.
    pub fn adapt_dataset(&self, inner: hdf5_reader::Dataset) -> Dataset {
        Dataset {
            inner,
            bytes: self.bytes.clone(),
            superblock: self.superblock.clone(),
        }
    }
}

/// Upstream dataset metadata with a fallback for affected filtered-index encodings.
pub(super) struct Dataset {
    /// Resolved dataset datatype, dimensions, attributes and fill value.
    inner: hdf5_reader::Dataset,
    /// Immutable source bytes shared with the upstream file.
    bytes: StorageBuffer,
    /// File-wide address and length widths.
    superblock: Superblock,
}

impl Deref for Dataset {
    type Target = hdf5_reader::Dataset;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl Dataset {
    /// Read a full numeric array, delegating every unaffected layout upstream.
    ///
    /// # Errors
    /// Returns upstream errors or bounded filtered-index decoding errors.
    pub fn read_array<T: H5Type>(&self) -> Result<ArrayD<T>> {
        if let Some(decoded) = self.repair(&hdf5_reader::SliceInfo::all(self.ndim()))? {
            self.typed(&decoded)
        } else {
            Ok(self.inner.read_array::<T>()?)
        }
    }

    /// Read stored-endian element bytes, including Alembic array/enum-wrapped POD types.
    ///
    /// # Errors
    /// Returns upstream errors or bounded filtered-index decoding errors.
    pub fn read_raw_bytes(&self) -> Result<Vec<u8>> {
        if let Some(decoded) = self.repair(&hdf5_reader::SliceInfo::all(self.ndim()))? {
            Ok(decoded.bytes)
        } else {
            Ok(self.inner.read_raw_bytes()?)
        }
    }

    /// Decode an affected scalar string through an upstream attribute object and heap resolver.
    ///
    /// # Errors
    /// Returns upstream errors for non-scalar strings, malformed data or unsupported encodings.
    pub fn read_string(&self) -> Result<String> {
        if self.num_elements()? != 1 {
            return Ok(self.inner.read_string()?);
        }
        let Some(decoded) = self.repair(&hdf5_reader::SliceInfo::all(self.ndim()))? else {
            return Ok(self.inner.read_string()?);
        };
        let header = self.header()?;
        let space = header
            .messages
            .iter()
            .find_map(|message| match message {
                HdfMessage::Dataspace(space) => Some(space.clone()),
                _ => None,
            })
            .ok_or("HDF string dataset lacks resolved dimensions")?;
        let message = hdf5_reader::messages::attribute::AttributeMessage {
            name: self.name().to_owned(),
            datatype: self.dtype().clone(),
            dataspace: space,
            raw_data: decoded.bytes,
        };
        Ok(hdf5_reader::Attribute::from_message_with_context(
            message,
            Some(&self.bytes),
            self.superblock.offset_size,
        )
        .read_string()?)
    }

    /// Read selected numeric samples, retaining first-frame and bounded-slice behavior.
    #[cfg(test)]
    fn read_slice<T: H5Type>(&self, selection: &hdf5_reader::SliceInfo) -> Result<ArrayD<T>> {
        if let Some(decoded) = self.repair(selection)? {
            self.typed(&decoded)
        } else {
            Ok(self.inner.read_slice::<T>(selection)?)
        }
    }

    /// Decode the assembled byte buffer with public upstream numeric conversion APIs.
    fn typed<T: H5Type>(&self, decoded: &Decoded) -> Result<ArrayD<T>> {
        let count = decoded.bytes.len() / decoded.element_size;
        let values = if let Some(values) = T::decode_vec(&decoded.bytes, self.dtype(), count) {
            values?
        } else {
            decoded
                .bytes
                .chunks_exact(decoded.element_size)
                .map(|bytes| T::from_bytes(bytes, self.dtype()))
                .collect::<hdf5_reader::error::Result<Vec<_>>>()?
        };
        Ok(ArrayD::from_shape_vec(IxDyn(&decoded.shape), values)?)
    }

    /// Parse and resolve object-header shared messages through public upstream APIs.
    fn header(&self) -> Result<ObjectHeader> {
        let mut header = ObjectHeader::parse_at(
            &self.bytes,
            self.address(),
            self.superblock.offset_size,
            self.superblock.length_size,
        )?;
        header.resolve_shared_messages(
            &self.bytes,
            self.superblock.offset_size,
            self.superblock.length_size,
        )?;
        Ok(header)
    }

    /// Resolve only the upstream-invalid size widths and decode intersecting chunks.
    #[allow(
        clippy::too_many_lines,
        reason = "The bounded fallback keeps metadata validation, allocation, upstream filter decoding and selected chunk assembly in one auditable data path."
    )]
    fn repair(&self, selection: &hdf5_reader::SliceInfo) -> Result<Option<Decoded>> {
        let Some(dims) = self.chunks() else {
            return Ok(None);
        };
        let header = self.header()?;
        let Some((address, kind)) = header.messages.iter().find_map(|message| {
            if let HdfMessage::DataLayout(layout) = message
                && let DataLayout::Chunked {
                    address,
                    chunk_indexing: Some(kind),
                    ..
                } = &layout.layout
            {
                return Some((*address, kind));
            }
            None
        }) else {
            return Ok(None);
        };
        if Cursor::is_undefined_offset(address, self.superblock.offset_size) {
            return Ok(None);
        }
        if dims.len() != self.ndim() || dims.is_empty() || dims.contains(&0) {
            return Err("HDF filtered chunk dimensions are invalid".into());
        }
        let index = chunks::Index {
            data: &self.bytes,
            offset_size: self.superblock.offset_size,
            length_size: self.superblock.length_size,
            shape: self.shape(),
            max_shape: self.max_dims(),
            dims: &dims,
        };
        let Some(entries) = index.entries(address, kind)? else {
            return Ok(None);
        };
        let element_size = self.inner.raw_element_size()?;
        if element_size == 0 {
            return Err("HDF numeric element size is zero".into());
        }
        let selected = Selection::new(self.shape(), selection)?;
        let output_size = selected
            .elements
            .checked_mul(element_size)
            .filter(|size| *size <= MAX_BYTES)
            .ok_or("HDF selected array exceeds 512 MiB")?;
        let chunk_elements = dims
            .iter()
            .try_fold(1usize, |total, &dim| {
                total.checked_mul(usize::try_from(dim).ok()?)
            })
            .ok_or("HDF chunk element count overflows")?;
        let chunk_bytes = chunk_elements
            .checked_mul(element_size)
            .filter(|size| *size <= MAX_BYTES)
            .ok_or("HDF chunk exceeds 512 MiB decoded limit")?;
        let filters = header
            .messages
            .iter()
            .find_map(|message| match message {
                HdfMessage::FilterPipeline(pipeline) => Some(pipeline.filters.as_slice()),
                _ => None,
            })
            .ok_or("HDF filtered index has no resolved filter pipeline")?;
        if filters.len() > 32 {
            return Err("HDF filter pipeline exceeds mask width".into());
        }
        let mut bytes = vec![0; output_size];
        if let Some(fill) = self.fill_value().and_then(|value| value.value.as_deref())
            && !fill.is_empty()
        {
            for destination in bytes.chunks_exact_mut(fill.len()) {
                destination.copy_from_slice(fill);
            }
        }
        for entry in entries {
            let Some(ranges) = selected.intersection(&entry.offsets, &dims, self.shape())? else {
                continue;
            };
            let size = if entry.size == 0 {
                chunk_bytes
            } else {
                usize::try_from(entry.size)?
            };
            let start = usize::try_from(entry.address)?;
            let end = start
                .checked_add(size)
                .ok_or("HDF encoded chunk range overflows")?;
            let encoded = self
                .bytes
                .get(start..end)
                .ok_or("HDF encoded chunk exceeds input")?;
            let decoded = apply_pipeline_with_limit(
                encoded,
                filters,
                entry.filter_mask,
                element_size,
                None,
                Some(chunk_bytes + 1),
            )?;
            if decoded.len() != chunk_bytes {
                return Err("HDF decoded chunk length mismatch".into());
            }
            selected.copy(
                &mut bytes,
                &decoded,
                element_size,
                &entry.offsets,
                &dims,
                &ranges,
            )?;
        }
        Ok(Some(Decoded {
            bytes,
            shape: selected.shape,
            element_size,
        }))
    }
}

/// Selected C-order bytes and the reduced result dimensions needed for upstream array construction.
struct Decoded {
    /// Assembled stored-endian element bytes.
    bytes: Vec<u8>,
    /// Result dimensions after index selections collapse axes.
    shape: Vec<usize>,
    /// Fixed numeric storage width.
    element_size: usize,
}

/// Validated per-axis start, step and selected sample count.
struct Axis {
    /// First selected dataset coordinate.
    start: u64,
    /// Distance between selected coordinates.
    step: u64,
    /// Number of selected samples along this axis, including collapsed axes.
    count: usize,
}

/// Bounded selection with one record per source axis and its reduced array shape.
struct Selection {
    /// Dataset axes in source C order.
    axes: Vec<Axis>,
    /// Output dimensions after individual index selections are removed.
    shape: Vec<usize>,
    /// Product of selected axis counts.
    elements: usize,
}

impl Selection {
    /// Resolve and bound every index/range before allocating data.
    fn new(shape: &[u64], selection: &hdf5_reader::SliceInfo) -> Result<Self> {
        if shape.len() != selection.selections.len() || shape.len() > 64 {
            return Err("HDF selection dimensionality is invalid".into());
        }
        let mut axes = Vec::new();
        let mut output = Vec::new();
        let mut elements = 1usize;
        for (&extent, item) in shape.iter().zip(&selection.selections) {
            let (start, step, count) = match *item {
                hdf5_reader::SliceInfoElem::Index(index) => {
                    if index >= extent {
                        return Err("HDF selection index exceeds dimension".into());
                    }
                    (index, 1, 1)
                }
                hdf5_reader::SliceInfoElem::Slice { start, end, step } => {
                    if step == 0 || start > extent {
                        return Err("invalid HDF selection range".into());
                    }
                    let count =
                        usize::try_from(end.min(extent).saturating_sub(start).div_ceil(step))?;
                    output.push(count);
                    (start, step, count)
                }
            };
            elements = elements
                .checked_mul(count)
                .filter(|count| *count <= MAX_SAMPLES)
                .ok_or("HDF selected array exceeds sample limit")?;
            axes.push(Axis { start, step, count });
        }
        Ok(Self {
            axes,
            shape: output,
            elements,
        })
    }

    /// Find selected sample-index intervals overlapping one allocated chunk.
    fn intersection(
        &self,
        offsets: &[u64],
        dims: &[u32],
        shape: &[u64],
    ) -> Result<Option<Vec<(usize, usize)>>> {
        if offsets.len() != self.axes.len() {
            return Err("HDF chunk offset rank is invalid".into());
        }
        let mut ranges = Vec::new();
        for (((axis, &offset), &dimension), &extent) in
            self.axes.iter().zip(offsets).zip(dims).zip(shape)
        {
            if offset >= extent || offset % u64::from(dimension) != 0 {
                return Err("HDF allocated chunk offset exceeds dataset".into());
            }
            let end = offset
                .checked_add(u64::from(dimension))
                .ok_or("HDF chunk extent overflows")?
                .min(extent);
            let first = usize::try_from(offset.saturating_sub(axis.start).div_ceil(axis.step))?
                .min(axis.count);
            let last = usize::try_from(end.saturating_sub(axis.start).div_ceil(axis.step))?
                .min(axis.count);
            if first >= last {
                return Ok(None);
            }
            ranges.push((first, last));
        }
        Ok(Some(ranges))
    }

    /// Copy intersecting selected elements while clipping padded chunk-edge samples.
    fn copy(
        &self,
        destination: &mut [u8],
        source: &[u8],
        element_size: usize,
        offsets: &[u64],
        dims: &[u32],
        ranges: &[(usize, usize)],
    ) -> Result<()> {
        let count = ranges
            .iter()
            .try_fold(1usize, |n, &(start, end)| n.checked_mul(end - start))
            .ok_or("HDF chunk selection overflows")?;
        for linear in 0..count {
            let mut remainder = linear;
            let mut source_index = 0usize;
            let mut destination_index = 0usize;
            let mut source_stride = 1usize;
            let mut destination_stride = 1usize;
            for axis in (0..ranges.len()).rev() {
                let (start, end) = ranges[axis];
                let index = start + remainder % (end - start);
                remainder /= end - start;
                let global = self.axes[axis].start + u64::try_from(index)? * self.axes[axis].step;
                source_index += usize::try_from(global - offsets[axis])? * source_stride;
                destination_index += index * destination_stride;
                source_stride *= usize::try_from(dims[axis])?;
                destination_stride *= self.axes[axis].count;
            }
            let input = source
                .get(source_index * element_size..(source_index + 1) * element_size)
                .ok_or("HDF selected chunk element is out of range")?;
            let output = destination
                .get_mut(destination_index * element_size..(destination_index + 1) * element_size)
                .ok_or("HDF selected array element is out of range")?;
            output.copy_from_slice(input);
        }
        Ok(())
    }
}

/// Upstream `NetCDF` metadata plus an optional shared HDF5 data adapter.
pub(super) struct NcFile {
    /// Classic or `NetCDF4` metadata and unaffected reads.
    inner: netcdf_reader::NcFile,
    /// Numeric fallback for HDF5-backed variables only.
    hdf: Option<Hdf5File>,
}

impl Deref for NcFile {
    type Target = netcdf_reader::NcFile;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl NcFile {
    /// Open classic or `NetCDF4` bytes with a shared bounded metadata/cache configuration.
    ///
    /// # Errors
    /// Returns upstream container and metadata errors.
    pub fn from_bytes_with_options(
        bytes: &[u8],
        options: netcdf_reader::NcOpenOptions,
    ) -> Result<Self> {
        let storage = Arc::new(BytesStorage::new(bytes.to_vec()));
        let inner = netcdf_reader::NcFile::from_storage_with_options(storage.clone(), options)?;
        let hdf = if bytes.starts_with(&hdf5_reader::superblock::HDF5_MAGIC) {
            Some(Hdf5File::from_storage(storage)?)
        } else {
            None
        };
        Ok(Self { inner, hdf })
    }

    /// Read typed data without converting integer connectivity through floating point.
    ///
    /// # Errors
    /// Returns upstream or bounded HDF5 numeric data errors.
    pub fn read_variable<T: NcReadable>(&self, name: &str) -> Result<ArrayD<T>> {
        self.variable(name)?;
        if let Some(file) = &self.hdf {
            let dataset = file.dataset(name)?;
            if let Some(decoded) = dataset.repair(&hdf5_reader::SliceInfo::all(dataset.ndim()))? {
                return dataset.typed(&decoded);
            }
        }
        Ok(self.inner.read_variable::<T>(name)?)
    }

    /// Promote stored numeric types using upstream typed arrays.
    ///
    /// # Errors
    /// Returns an error for unsupported numeric datatypes or failed data reads.
    pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
        self.variable(name)?;
        if let Some(file) = &self.hdf
            && let Some(values) = numeric(&file.dataset(name)?, None)?
        {
            return Ok(values);
        }
        Ok(self.inner.read_variable_as_f64(name)?)
    }

    /// Apply upstream CF unpacking after an owned affected-index read.
    ///
    /// # Errors
    /// Returns variable lookup, datatype and bounded data-reading errors.
    pub fn read_variable_unpacked(&self, name: &str) -> Result<ArrayD<f64>> {
        let mut values = self.read_variable_as_f64(name)?;
        if let Some(parameters) =
            netcdf_reader::unpack::UnpackParams::from_variable(self.variable(name)?)
        {
            parameters.apply(&mut values);
        }
        Ok(values)
    }

    /// Read a slice and apply upstream masking before upstream scale/offset unpacking.
    ///
    /// # Errors
    /// Returns invalid selection, metadata, datatype and bounded data-reading errors.
    pub fn read_variable_slice_unpacked_masked(
        &self,
        name: &str,
        selection: &NcSliceInfo,
    ) -> Result<ArrayD<f64>> {
        let variable = self.variable(name)?;
        if let Some(file) = &self.hdf {
            let hdf_selection = hdf5_reader::SliceInfo {
                selections: selection
                    .selections
                    .iter()
                    .map(|selection| match *selection {
                        NcSliceInfoElem::Index(index) => hdf5_reader::SliceInfoElem::Index(index),
                        NcSliceInfoElem::Slice { start, end, step } => {
                            hdf5_reader::SliceInfoElem::Slice { start, end, step }
                        }
                    })
                    .collect(),
            };
            if let Some(mut values) = numeric(&file.dataset(name)?, Some(&hdf_selection))? {
                if let Some(parameters) = netcdf_reader::masked::MaskParams::from_variable(variable)
                {
                    parameters.apply(&mut values);
                }
                if let Some(parameters) =
                    netcdf_reader::unpack::UnpackParams::from_variable(variable)
                {
                    parameters.apply(&mut values);
                }
                return Ok(values);
            }
        }
        Ok(self
            .inner
            .read_variable_slice_unpacked_masked(name, selection)?)
    }
}

/// Read supported numeric storage with exact upstream decoding followed by f64 promotion.
#[allow(
    clippy::cast_precision_loss,
    clippy::cast_lossless,
    reason = "All numeric widths share one dispatch; 64-bit geometry scalars intentionally approximate to f64."
)]
fn numeric(
    dataset: &Dataset,
    selection: Option<&hdf5_reader::SliceInfo>,
) -> Result<Option<ArrayD<f64>>> {
    let full = hdf5_reader::SliceInfo::all(dataset.ndim());
    let Some(decoded) = dataset.repair(selection.unwrap_or(&full))? else {
        return Ok(None);
    };
    macro_rules! read {
        ($type:ty) => {{
            let values = dataset.typed::<$type>(&decoded)?;
            Ok(Some(values.mapv(|value| value as f64)))
        }};
    }
    match dataset.dtype() {
        Datatype::FloatingPoint { size: 4, .. } => read!(f32),
        Datatype::FloatingPoint { size: 8, .. } => read!(f64),
        Datatype::FixedPoint {
            size: 1,
            signed: true,
            ..
        } => read!(i8),
        Datatype::FixedPoint {
            size: 1,
            signed: false,
            ..
        } => read!(u8),
        Datatype::FixedPoint {
            size: 2,
            signed: true,
            ..
        } => read!(i16),
        Datatype::FixedPoint {
            size: 2,
            signed: false,
            ..
        } => read!(u16),
        Datatype::FixedPoint {
            size: 4,
            signed: true,
            ..
        } => read!(i32),
        Datatype::FixedPoint {
            size: 4,
            signed: false,
            ..
        } => read!(u32),
        Datatype::FixedPoint {
            size: 8,
            signed: true,
            ..
        } => read!(i64),
        Datatype::FixedPoint {
            size: 8,
            signed: false,
            ..
        } => read!(u64),
        _ => Err("HDF scientific array has an unsupported numeric datatype".into()),
    }
}

#[cfg(test)]
#[path = "hdf5_owned_tests.rs"]
/// Regressions exercising unmodified upstream errors and the owned affected-layout path.
mod tests;

#[cfg(test)]
#[path = "hdf5_index_tests.rs"]
/// Independently authored HDF Group hierarchy and physical-page metadata regressions.
mod index_tests;