Skip to main content

rust_hdf5/format/
selection.rs

1//! Serialized H5S dataspace-selection decoder.
2//!
3//! `H5S_select_deserialize` (H5Sselect.c) and its per-type callbacks
4//! (H5Sall.c, H5Snone.c, H5Shyper.c) define a small self-describing wire
5//! format for "which elements of a dataspace are selected". It shows up
6//! embedded inside other structures — a Virtual Dataset mapping entry
7//! (H5Dvirtual.c `H5D__virtual_load_layout`) carries two of them per
8//! mapping, and a region reference embeds one too — so this module decodes
9//! only the selection bytes themselves and knows nothing about either
10//! caller.
11//!
12//! Binary layout, common header:
13//! ```text
14//! sel_type: u32 LE (0 = none, 1 = points, 2 = hyperslabs, 3 = all)
15//! ```
16//! followed by a type-specific body.
17//!
18//! All / None body (version is always 1):
19//! ```text
20//! version:  u32 LE (= 1)
21//! reserved: 8 bytes
22//! ```
23//!
24//! Point body (`H5S__point_deserialize`, H5Spoint.c):
25//! ```text
26//! version: u32 LE (1 or 2)
27//! if version >= 2: enc_size: 1 byte (2, 4, or 8 bytes)
28//! else (version == 1): padding: 4 bytes, length: 4 bytes, enc_size = 4
29//! rank: u32 LE
30//! num_points: enc_size bytes LE
31//! num_points * rank * { coordinate }, each enc_size bytes LE (point-major,
32//! coordinate-minor — point 0's rank coordinates, then point 1's, ...)
33//! ```
34//! The version-1 `length` field records the byte count from `rank` to the
35//! end of the point list (`8 + num_points * rank * 4`); this module does
36//! not validate it on decode (H5S__point_deserialize doesn't either), but
37//! [`Selection::encode`] reproduces the exact value libhdf5 writes there,
38//! since a byte-for-byte comparison against a captured image needs it.
39//!
40//! Hyperslab body:
41//! ```text
42//! version: u32 LE (1, 2, or 3)
43//! if version >= 2: flags: 1 byte (bit 0 = REGULAR)
44//! if version >= 3: enc_size: 1 byte (tag 0x02/0x04/0x08 => 2/4/8 bytes)
45//! else if version == 2: reserved: 4 bytes, enc_size = 8
46//! else (version == 1): reserved: 8 bytes, enc_size = 4
47//! rank: u32 LE
48//! if the REGULAR flag is set (only possible for version >= 2):
49//!   rank * { start, stride, count, block }, each enc_size bytes LE
50//!   (an all-ones count or block of that width means H5S_UNLIMITED)
51//! else (block list, any version):
52//!   num_blocks: enc_size bytes LE
53//!   num_blocks * { rank * start_coord, rank * end_coord }, each
54//!   enc_size bytes LE (absolute element coordinates, end inclusive,
55//!   blocks combined by union)
56//! ```
57//!
58//! Empirically confirmed against libhdf5 1.14.6 (h5py's `VirtualLayout`
59//! writes hyperslab selections in the version-1 block-list form — with
60//! exactly one block — even for a selection that is mathematically a
61//! single regular block; see [`h5py_single_block_selection_is_version_one`]).
62
63use crate::format::{FormatError, FormatResult};
64
65/// Sentinel marking a hyperslab `count` or `block` value as
66/// unlimited/growable (`H5S_UNLIMITED`, numerically `HSIZE_UNDEF` —
67/// H5Spublic.h / H5public.h).
68pub const UNLIMITED: u64 = u64::MAX;
69
70const SEL_NONE: u32 = 0;
71const SEL_POINTS: u32 = 1;
72const SEL_HYPERSLABS: u32 = 2;
73const SEL_ALL: u32 = 3;
74
75const ALL_NONE_VERSION: u32 = 1;
76
77const HYPER_VERSION_1: u32 = 1;
78const HYPER_VERSION_2: u32 = 2;
79const HYPER_VERSION_3: u32 = 3;
80
81const HYPER_REGULAR_FLAG: u8 = 0x01;
82
83const POINT_VERSION_1: u32 = 1;
84const POINT_VERSION_2: u32 = 2;
85
86/// The dataspace rank ceiling libhdf5 enforces (`H5S_MAX_RANK`,
87/// H5Spublic.h). Bounds `rank`-sized allocations before any element count
88/// derived from the file is trusted.
89const MAX_RANK: usize = 32;
90
91/// One block of a hyperslab block-list selection: inclusive
92/// `start..=end` element coordinates, one pair per dimension.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct HyperslabBlock {
95    pub start: Vec<u64>,
96    pub end: Vec<u64>,
97}
98
99/// A regular (start, stride, count, block) hyperslab: one tuple per
100/// dimension. `count`/`block` may hold [`UNLIMITED`].
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct RegularHyperslab {
103    pub start: Vec<u64>,
104    pub stride: Vec<u64>,
105    pub count: Vec<u64>,
106    pub block: Vec<u64>,
107}
108
109impl RegularHyperslab {
110    /// The one dimension whose `count` or `block` is [`UNLIMITED`], or `None`
111    /// when the selection is bounded — `H5S_get_select_unlim_dim`, which
112    /// returns a single dimension because `H5Sselect_hyperslab` refuses a
113    /// second unlimited one.
114    pub fn unlim_dim(&self) -> Option<usize> {
115        (0..self.count.len())
116            .find(|&d| self.count[d] == UNLIMITED || self.block.get(d) == Some(&UNLIMITED))
117    }
118
119    /// The `(count, block)` this dimension takes once its extent is known —
120    /// `H5S__hyper_get_clip_diminfo`. A zero in either field means the
121    /// selection ends up empty.
122    fn clip_diminfo(start: u64, stride: u64, count: u64, block: u64, clip_size: u64) -> (u64, u64) {
123        if start >= clip_size {
124            if block == UNLIMITED {
125                (count, 0)
126            } else {
127                (0, block)
128            }
129        } else if block == UNLIMITED || block == stride {
130            (1, clip_size - start)
131        } else {
132            // The other unlimited form: an unbounded count of fixed blocks.
133            let stride = stride.max(1);
134            ((clip_size - start).div_ceil(stride), block)
135        }
136    }
137
138    /// How many slices of the unlimited dimension this selection covers when
139    /// its dataspace extent is `clip_size` — the slice count
140    /// `H5S_hyper_get_clip_extent_match` computes from the *match* space
141    /// before handing it to [`Self::clip_extent`].
142    pub fn num_slices(&self, clip_size: u64) -> u64 {
143        let Some(d) = self.unlim_dim() else {
144            return 0;
145        };
146        let (start, stride) = (self.start[d], self.stride[d]);
147        let (count, block) =
148            Self::clip_diminfo(start, stride, self.count[d], self.block[d], clip_size);
149        if block == 0 || count == 0 {
150            return 0;
151        }
152        if count == 1 {
153            return block;
154        }
155        let span = stride * (count - 1) + block;
156        let avail = clip_size - start;
157        if span > avail {
158            block * count - (span - avail)
159        } else {
160            block * count
161        }
162    }
163
164    /// The virtual extent that makes this selection cover exactly `num_slices`
165    /// slices of its unlimited dimension — `H5S__hyper_get_clip_extent_real`.
166    /// `incl_trail` is the `H5D_VDS_FIRST_MISSING` view; the default
167    /// `H5D_VDS_LAST_AVAILABLE` passes `false`.
168    pub fn clip_extent(&self, num_slices: u64, incl_trail: bool) -> u64 {
169        let Some(d) = self.unlim_dim() else {
170            return 0;
171        };
172        let (start, stride, block) = (self.start[d], self.stride[d], self.block[d]);
173        if num_slices == 0 {
174            return if incl_trail { start } else { 0 };
175        }
176        if block == UNLIMITED || block == stride {
177            return start + num_slices;
178        }
179        let block = block.max(1);
180        let count = num_slices / block;
181        let rem = num_slices - count * block;
182        if rem > 0 {
183            start + count * stride + rem
184        } else if incl_trail {
185            start + count * stride
186        } else {
187            start + (count - 1) * stride + block
188        }
189    }
190
191    /// Elements in one slice through the dimensions that are *not* the
192    /// unlimited one — `H5S_get_select_num_elem_non_unlim`, the quantity two
193    /// unlimited selections in one mapping must agree on.
194    pub fn num_elem_non_unlim(&self) -> Option<u64> {
195        let d = self.unlim_dim()?;
196        (0..self.count.len())
197            .filter(|&i| i != d)
198            .try_fold(1u64, |acc, i| {
199                acc.checked_mul(self.count[i])?.checked_mul(self.block[i])
200            })
201    }
202
203    /// Block `index` of the unlimited dimension on its own, every other
204    /// dimension left as it is — `H5S_hyper_get_unlim_block`, the selection a
205    /// printf mapping's `index`-th source dataset fills.
206    pub fn unlim_block(&self, index: u64) -> Self {
207        let mut out = self.clone();
208        if let Some(d) = self.unlim_dim() {
209            out.start[d] = self.start[d] + index * self.stride[d];
210            out.count[d] = 1;
211        }
212        out
213    }
214}
215
216/// The two wire forms a hyperslab selection can take
217/// (`H5S__hyper_deserialize`, H5Shyper.c): a single compact
218/// (start, stride, count, block) tuple per dimension, or an explicit list
219/// of blocks combined by union. libhdf5 always writes the block-list form
220/// for a selection made of exactly one block — including h5py's
221/// `VirtualLayout` — so both forms are real on-disk data, not just
222/// alternates on paper.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum Hyperslab {
225    Regular(RegularHyperslab),
226    Blocks(Vec<HyperslabBlock>),
227}
228
229/// An explicit list of selected element coordinates (`H5S_SEL_POINTS`):
230/// one `rank`-length coordinate vector per point, in selection order.
231/// Selection order is significant for a pointwise iterator (it is the
232/// linear order in which elements are visited), so callers must not
233/// reorder `points`.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct PointSelection {
236    pub rank: usize,
237    pub points: Vec<Vec<u64>>,
238}
239
240/// One maximal run of consecutive selected elements, as it sits both in the
241/// box it came from and in the full extent that box was resolved against.
242///
243/// A run is contiguous in the fastest-varying dimension, so it is one
244/// unbroken stretch of elements in a row-major buffer of either shape — the
245/// unit a transfer can move with a single copy.
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub(crate) struct SelectionRun {
248    /// Index into [`ResolvedSelection::boxes`] of the box this run lies in.
249    pub box_index: usize,
250    /// Element offset of the run's first element within a row-major buffer
251    /// shaped like that box.
252    pub offset_in_box: u64,
253    /// Element offset of the run's first element within a row-major buffer
254    /// shaped like the whole extent.
255    pub offset_in_extent: u64,
256    /// How many consecutive elements the run covers.
257    pub len: u64,
258}
259
260/// A selection bound to a concrete extent — [`Selection::resolve`].
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub(crate) struct ResolvedSelection {
263    /// The disjoint `(start, count)` boxes covering the selected elements,
264    /// in [`Selection::to_boxes`] order.
265    pub boxes: Vec<(Vec<u64>, Vec<u64>)>,
266    /// Every selected element exactly once, grouped into runs and ordered
267    /// the way H5S's own selection iterator visits them.
268    pub runs: Vec<SelectionRun>,
269}
270
271impl ResolvedSelection {
272    /// How many elements the selection holds — `H5S_GET_SELECT_NPOINTS`.
273    pub(crate) fn n_elements(&self) -> u64 {
274        self.runs.iter().map(|r| r.len).sum()
275    }
276}
277
278/// Row-major element strides of an extent: `strides[d]` is how far one step
279/// in dimension `d` moves within a densely-packed buffer of shape `dims`.
280fn row_major_strides(dims: &[u64]) -> FormatResult<Vec<u64>> {
281    let mut strides = vec![1u64; dims.len()];
282    for d in (0..dims.len().saturating_sub(1)).rev() {
283        strides[d] = strides[d + 1].checked_mul(dims[d + 1]).ok_or_else(|| {
284            FormatError::InvalidData(format!(
285                "extent {dims:?} holds more elements than u64 counts"
286            ))
287        })?;
288    }
289    Ok(strides)
290}
291
292/// Append the runs one box contributes, in the box's own row-major order.
293///
294/// Consecutive runs that are adjacent in *both* the box and the extent are
295/// merged into one, which is the "trailing dimension fully selected on both
296/// sides" coalescing a dual-array walk performs: a box covering a whole
297/// trailing region comes out as a single run rather than one run per row.
298fn push_box_runs(
299    box_index: usize,
300    start: &[u64],
301    count: &[u64],
302    dims: &[u64],
303    strides: &[u64],
304    runs: &mut Vec<SelectionRun>,
305) -> FormatResult<()> {
306    let rank = dims.len();
307    if start.len() != rank || count.len() != rank {
308        return Err(FormatError::InvalidData(format!(
309            "selection box of rank {} against a {rank}-dimensional extent",
310            start.len()
311        )));
312    }
313    for d in 0..rank {
314        let end = start[d]
315            .checked_add(count[d])
316            .ok_or_else(|| FormatError::InvalidData("selection box coordinate overflows".into()))?;
317        if end > dims[d] {
318            return Err(FormatError::InvalidData(format!(
319                "selection box covers [{}, {end}) of dimension {d}, past the extent {}",
320                start[d], dims[d]
321            )));
322        }
323    }
324    if count.contains(&0) {
325        return Ok(());
326    }
327    if rank == 0 {
328        // A scalar dataspace holds exactly one element and admits only the
329        // all/none selections, so its one box is one run of one element.
330        runs.push(SelectionRun {
331            box_index,
332            offset_in_box: 0,
333            offset_in_extent: 0,
334            len: 1,
335        });
336        return Ok(());
337    }
338    let box_strides = row_major_strides(count)?;
339    let run_len = count[rank - 1];
340    let n_outer = count[..rank - 1]
341        .iter()
342        .try_fold(1u64, |acc, &c| acc.checked_mul(c))
343        .ok_or_else(|| FormatError::InvalidData("selection box element count overflows".into()))?;
344    let mut coords = vec![0u64; rank - 1];
345    for _ in 0..n_outer {
346        let mut offset_in_extent = start[rank - 1];
347        let mut offset_in_box = 0u64;
348        for d in 0..rank - 1 {
349            offset_in_extent += (start[d] + coords[d]) * strides[d];
350            offset_in_box += coords[d] * box_strides[d];
351        }
352        match runs.last_mut() {
353            Some(prev)
354                if prev.box_index == box_index
355                    && prev.offset_in_extent + prev.len == offset_in_extent
356                    && prev.offset_in_box + prev.len == offset_in_box =>
357            {
358                prev.len += run_len;
359            }
360            _ => runs.push(SelectionRun {
361                box_index,
362                offset_in_box,
363                offset_in_extent,
364                len: run_len,
365            }),
366        }
367        for d in (0..rank - 1).rev() {
368            coords[d] += 1;
369            if coords[d] < count[d] {
370                break;
371            }
372            coords[d] = 0;
373        }
374    }
375    Ok(())
376}
377
378/// A decoded H5S dataspace selection.
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub enum Selection {
381    /// `H5S_SEL_NONE`: no elements selected.
382    None,
383    /// `H5S_SEL_ALL`: every element of whatever dataspace this selection
384    /// is later bound against.
385    All,
386    /// `H5S_SEL_HYPERSLABS`.
387    Hyperslab { rank: usize, form: Hyperslab },
388    /// `H5S_SEL_POINTS`.
389    Points(PointSelection),
390}
391
392impl Selection {
393    /// Decode one serialized selection from the front of `buf`.
394    ///
395    /// Returns the selection and the number of bytes consumed; `buf` may
396    /// have trailing bytes belonging to whatever follows (a VDS mapping
397    /// entry decodes a source selection immediately followed by a virtual
398    /// selection out of the same buffer, for instance).
399    pub fn decode(buf: &[u8]) -> FormatResult<(Self, usize)> {
400        if buf.len() < 4 {
401            return Err(FormatError::BufferTooShort {
402                needed: 4,
403                available: buf.len(),
404            });
405        }
406        let sel_type = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
407        let body = &buf[4..];
408        match sel_type {
409            SEL_NONE => {
410                let consumed = decode_all_none_body(body)?;
411                Ok((Self::None, 4 + consumed))
412            }
413            SEL_ALL => {
414                let consumed = decode_all_none_body(body)?;
415                Ok((Self::All, 4 + consumed))
416            }
417            SEL_HYPERSLABS => {
418                let (rank, form, consumed) = decode_hyperslab_body(body)?;
419                Ok((Self::Hyperslab { rank, form }, 4 + consumed))
420            }
421            SEL_POINTS => {
422                let (points, consumed) = decode_points_body(body)?;
423                Ok((Self::Points(points), 4 + consumed))
424            }
425            other => Err(FormatError::InvalidData(format!(
426                "unknown dataspace selection type {other}"
427            ))),
428        }
429    }
430
431    /// Decompose this selection into `(start, count)` boxes — each
432    /// `dims.len()` elements wide — whose union is exactly the selected
433    /// elements, with no overlap between boxes. `dims` supplies the full
434    /// extent for [`Selection::All`] and must match a hyperslab's `rank`;
435    /// it is not otherwise consulted (coordinates are absolute, so this
436    /// does not clip an out-of-range block against `dims` — a caller that
437    /// needs that validates it itself).
438    ///
439    /// `Err` for a regular hyperslab whose `count` or `block` holds
440    /// [`UNLIMITED`]: an unbounded dimension cannot become a finite box
441    /// without a growable-dataset extent this call does not have.
442    pub fn to_boxes(&self, dims: &[u64]) -> FormatResult<Vec<(Vec<u64>, Vec<u64>)>> {
443        match self {
444            Self::None => Ok(Vec::new()),
445            Self::All => Ok(vec![(vec![0u64; dims.len()], dims.to_vec())]),
446            Self::Points(ps) => {
447                if ps.rank != dims.len() {
448                    return Err(FormatError::InvalidData(format!(
449                        "point selection rank {} does not match the {}-dimensional extent",
450                        ps.rank,
451                        dims.len()
452                    )));
453                }
454                // Each point is its own 1-element box in every dimension —
455                // the only axis-aligned decomposition that holds in
456                // general for an arbitrary scatter of coordinates.
457                Ok(ps
458                    .points
459                    .iter()
460                    .map(|p| (p.clone(), vec![1u64; ps.rank]))
461                    .collect())
462            }
463            Self::Hyperslab { rank, form } => {
464                if *rank != dims.len() {
465                    return Err(FormatError::InvalidData(format!(
466                        "hyperslab selection rank {rank} does not match the \
467                         {}-dimensional extent",
468                        dims.len()
469                    )));
470                }
471                match form {
472                    Hyperslab::Blocks(blocks) => blocks
473                        .iter()
474                        .map(|b| {
475                            let count = b
476                                .start
477                                .iter()
478                                .zip(&b.end)
479                                .map(|(&s, &e)| {
480                                    e.checked_sub(s).and_then(|d| d.checked_add(1)).ok_or_else(
481                                        || {
482                                            FormatError::InvalidData(
483                                                "hyperslab block end precedes its start".into(),
484                                            )
485                                        },
486                                    )
487                                })
488                                .collect::<FormatResult<Vec<u64>>>()?;
489                            Ok((b.start.clone(), count))
490                        })
491                        .collect(),
492                    Hyperslab::Regular(r) => regular_hyperslab_to_boxes(r),
493                }
494            }
495        }
496    }
497
498    /// Bind this selection to a concrete extent: the boxes it covers plus
499    /// the element runs those boxes contribute, in H5S selection order.
500    ///
501    /// Where [`to_boxes`](Self::to_boxes) answers "which regions", this
502    /// answers "which elements, in which order" — the order
503    /// `H5S_select_iter_next` walks a selection in, which is what pairs one
504    /// selection with another. `H5S_select_project_intersection`
505    /// (H5Sselect.c:2402) matches a virtual dataset mapping's two selections
506    /// off one element against one element in exactly this order, and asks
507    /// of them only `H5S_GET_SELECT_NPOINTS(src) == NPOINTS(dst)` — the same
508    /// single condition `H5D_virtual_check_mapping_pre` enforces when the
509    /// mapping is created (H5Dvirtual.c:254-257). Neither the two ranks nor
510    /// the two box decompositions need agree.
511    ///
512    /// Unlike [`to_boxes`](Self::to_boxes) this *is* the caller that
513    /// validates against `dims`: a run's offset within the extent only means
514    /// anything if its box lies inside it, so a box reaching past `dims` is
515    /// [`FormatError::InvalidData`] rather than an offset pointing at some
516    /// other element.
517    pub(crate) fn resolve(&self, dims: &[u64]) -> FormatResult<ResolvedSelection> {
518        let boxes = self.to_boxes(dims)?;
519        let strides = row_major_strides(dims)?;
520        let mut runs = Vec::new();
521        for (i, (start, count)) in boxes.iter().enumerate() {
522            push_box_runs(i, start, count, dims, &strides, &mut runs)?;
523        }
524        // Every selection but a point list is walked in increasing row-major
525        // coordinate order, and the runs of one box come out in that order
526        // already; ordering the boxes against each other is what the sort
527        // adds. Disjoint boxes hold disjoint elements, so their runs hold
528        // disjoint offset intervals and sorting by the first offset is a
529        // total order. A point list keeps the order its coordinates were
530        // given in (`H5S__point_iter_next`, H5Spoint.c) and must not be
531        // sorted — selection order is the point order.
532        if !matches!(self, Self::Points(_)) {
533            runs.sort_unstable_by_key(|r| r.offset_in_extent);
534        }
535        Ok(ResolvedSelection { boxes, runs })
536    }
537
538    /// Encode this selection into its `H5S_select_serialize` wire form
539    /// (H5Sselect.c and the per-type callbacks in H5Sall.c/H5Snone.c/
540    /// H5Spoint.c/H5Shyper.c).
541    ///
542    /// This module has no file context (no libver bounds), so it always
543    /// targets the version libhdf5 itself picks for the *default* low
544    /// format-version bound (`H5F_LIBVER_V18`, `H5F_ACS_LIBVER_LOW_BOUND_DEF`
545    /// in H5Pfapl.c) with no huge counts or coordinates: version 1 for
546    /// points, and version 1 (the block-list wire form) for hyperslabs —
547    /// `H5S__hyper_get_version_enc_size` picks version 1 for *any*
548    /// hyperslab (regular or not) whenever the low bound is below
549    /// `H5F_LIBVER_V112`, so [`Hyperslab::Regular`] is decomposed into
550    /// blocks first ([`Selection::to_boxes`]'s own expansion) rather than
551    /// written with the version-2/3 REGULAR flag. Confirmed byte-for-byte
552    /// against libhdf5-captured `H5Sencode2` images — see
553    /// `selection_matches_libhdf5_image` in this module's tests.
554    ///
555    /// A selection that cannot be expressed in that version-1, 4-byte-wide
556    /// form — more than `u32::MAX` points/blocks, or a coordinate that
557    /// does not fit `u32` — is [`FormatError::UnsupportedFeature`], not
558    /// silently truncated.
559    pub fn encode(&self) -> FormatResult<Vec<u8>> {
560        match self {
561            Self::None => Ok(encode_all_none(SEL_NONE)),
562            Self::All => Ok(encode_all_none(SEL_ALL)),
563            Self::Points(ps) => encode_points(ps),
564            Self::Hyperslab { rank, form } => encode_hyperslab(*rank, form),
565        }
566    }
567
568    /// The one dimension this selection grows in, or `None` when it is
569    /// bounded — `H5S_get_select_unlim_dim`. Only a regular hyperslab can
570    /// carry `H5S_UNLIMITED` at all (`H5Sselect_hyperslab` refuses it for
571    /// every other form), so every other selection answers `None`.
572    pub fn unlim_dim(&self) -> Option<usize> {
573        match self {
574            Self::Hyperslab {
575                form: Hyperslab::Regular(r),
576                ..
577            } => r.unlim_dim(),
578            _ => None,
579        }
580    }
581
582    /// This selection with its unlimited dimension clipped to an extent of
583    /// `clip_size` — `H5S_hyper_clip_unlim` (H5Shyper.c), which is how a
584    /// virtual dataset's unlimited mapping becomes the finite one a read
585    /// walks.
586    ///
587    /// The result is a block list rather than a regular hyperslab because the
588    /// last block may be cut in half by the extent: upstream builds a span
589    /// tree and intersects it with `clip_size` for exactly that case, and an
590    /// explicit block list is the same set of elements. A selection that
591    /// clips away to nothing becomes [`Selection::None`]; a bounded selection
592    /// is returned unchanged.
593    pub fn clip_unlimited(&self, clip_size: u64) -> FormatResult<Self> {
594        let Self::Hyperslab {
595            rank,
596            form: Hyperslab::Regular(r),
597        } = self
598        else {
599            return Ok(self.clone());
600        };
601        let Some(d) = r.unlim_dim() else {
602            return Ok(self.clone());
603        };
604        let (count, block) = RegularHyperslab::clip_diminfo(
605            r.start[d],
606            r.stride[d],
607            r.count[d],
608            r.block[d],
609            clip_size,
610        );
611        if count == 0 || block == 0 {
612            return Ok(Self::None);
613        }
614        let mut clipped = r.clone();
615        clipped.count[d] = count;
616        clipped.block[d] = block;
617
618        let mut blocks = Vec::new();
619        for (start, count) in regular_hyperslab_to_boxes(&clipped)? {
620            // Trim the block the extent cuts through, and drop one that
621            // starts past it — the intersection upstream's span tree takes
622            // against `block[unlim_dim] = clip_size`.
623            if start[d] >= clip_size {
624                continue;
625            }
626            let mut count = count;
627            count[d] = count[d].min(clip_size - start[d]);
628            let end = start
629                .iter()
630                .zip(&count)
631                .map(|(&s, &c)| s + c - 1)
632                .collect::<Vec<u64>>();
633            blocks.push(HyperslabBlock { start, end });
634        }
635        if blocks.is_empty() {
636            return Ok(Self::None);
637        }
638        Ok(Self::Hyperslab {
639            rank: *rank,
640            form: Hyperslab::Blocks(blocks),
641        })
642    }
643
644    /// The inclusive bounding box of the selection — `H5Sget_select_bounds`
645    /// — or `None` when the selection has no bounds of its own:
646    /// [`Selection::All`] takes the extent it is bound against,
647    /// [`Selection::None`] selects nothing, and a regular hyperslab with an
648    /// [`UNLIMITED`] count or block has no upper bound until a growable
649    /// extent supplies one.
650    pub fn bounds(&self) -> Option<(Vec<u64>, Vec<u64>)> {
651        match self {
652            Self::All | Self::None => None,
653            Self::Hyperslab { rank, form } => {
654                let blocks = hyperslab_to_block_list(*rank, form).ok()?;
655                let first = blocks.first()?;
656                let mut lo = first.start.clone();
657                let mut hi = first.end.clone();
658                for b in &blocks[1..] {
659                    for (l, s) in lo.iter_mut().zip(&b.start) {
660                        *l = (*l).min(*s);
661                    }
662                    for (h, e) in hi.iter_mut().zip(&b.end) {
663                        *h = (*h).max(*e);
664                    }
665                }
666                Some((lo, hi))
667            }
668            Self::Points(ps) => {
669                let first = ps.points.first()?;
670                let mut lo = first.clone();
671                let mut hi = first.clone();
672                for p in &ps.points[1..] {
673                    for (l, c) in lo.iter_mut().zip(p) {
674                        *l = (*l).min(*c);
675                    }
676                    for (h, c) in hi.iter_mut().zip(p) {
677                        *h = (*h).max(*c);
678                    }
679                }
680                Some((lo, hi))
681            }
682        }
683    }
684}
685
686/// Cast a decoded/caller-supplied coordinate down to the 4-byte width
687/// this module's version-1-only [`Selection::encode`] writes.
688fn to_u32(v: u64) -> FormatResult<u32> {
689    u32::try_from(v).map_err(|_| {
690        FormatError::UnsupportedFeature(format!(
691            "value {v} exceeds the 4-byte encoding Selection::encode targets (version 1, \
692             matching libhdf5's default H5F_LIBVER_V18 low format-version bound)"
693        ))
694    })
695}
696
697fn encode_all_none(sel_type: u32) -> Vec<u8> {
698    let mut buf = Vec::with_capacity(16);
699    buf.extend_from_slice(&sel_type.to_le_bytes());
700    buf.extend_from_slice(&ALL_NONE_VERSION.to_le_bytes());
701    buf.extend_from_slice(&[0u8; 8]);
702    buf
703}
704
705fn encode_points(ps: &PointSelection) -> FormatResult<Vec<u8>> {
706    if ps.rank == 0 || ps.rank > MAX_RANK {
707        return Err(FormatError::InvalidData(format!(
708            "invalid point selection rank {}",
709            ps.rank
710        )));
711    }
712    for p in &ps.points {
713        if p.len() != ps.rank {
714            return Err(FormatError::InvalidData(format!(
715                "point selection coordinate length {} does not match rank {}",
716                p.len(),
717                ps.rank
718            )));
719        }
720    }
721    let num_points: u32 = ps.points.len().try_into().map_err(|_| {
722        FormatError::UnsupportedFeature(format!(
723            "point selection has {} points, too many for version-1 encode (u32 count)",
724            ps.points.len()
725        ))
726    })?;
727    let rank_u32 = ps.rank as u32;
728    let payload_bytes: u32 = num_points
729        .checked_mul(4)
730        .and_then(|v| v.checked_mul(rank_u32))
731        .ok_or_else(|| {
732            FormatError::UnsupportedFeature(
733                "point selection payload too large for version-1 encode".into(),
734            )
735        })?;
736    let len = 8u32.checked_add(payload_bytes).ok_or_else(|| {
737        FormatError::UnsupportedFeature(
738            "point selection payload too large for version-1 encode".into(),
739        )
740    })?;
741
742    let mut buf = Vec::with_capacity(24 + payload_bytes as usize);
743    buf.extend_from_slice(&SEL_POINTS.to_le_bytes());
744    buf.extend_from_slice(&POINT_VERSION_1.to_le_bytes());
745    buf.extend_from_slice(&0u32.to_le_bytes()); // padding
746    buf.extend_from_slice(&len.to_le_bytes());
747    buf.extend_from_slice(&rank_u32.to_le_bytes());
748    buf.extend_from_slice(&num_points.to_le_bytes());
749    for p in &ps.points {
750        for &c in p {
751            buf.extend_from_slice(&to_u32(c)?.to_le_bytes());
752        }
753    }
754    Ok(buf)
755}
756
757/// Normalize either wire form of a hyperslab selection into an explicit
758/// block list — the form [`encode_hyperslab`] writes regardless of which
759/// form `form` holds (see [`Selection::encode`]'s doc comment for why).
760fn hyperslab_to_block_list(rank: usize, form: &Hyperslab) -> FormatResult<Vec<HyperslabBlock>> {
761    match form {
762        Hyperslab::Blocks(blocks) => {
763            for b in blocks {
764                if b.start.len() != rank || b.end.len() != rank {
765                    return Err(FormatError::InvalidData(format!(
766                        "hyperslab block coordinate length does not match rank {rank}"
767                    )));
768                }
769            }
770            Ok(blocks.clone())
771        }
772        Hyperslab::Regular(r) => {
773            if r.start.len() != rank
774                || r.stride.len() != rank
775                || r.count.len() != rank
776                || r.block.len() != rank
777            {
778                return Err(FormatError::InvalidData(format!(
779                    "regular hyperslab field length does not match rank {rank}"
780                )));
781            }
782            regular_hyperslab_to_boxes(r)?
783                .into_iter()
784                .map(|(start, count)| {
785                    let end = start
786                        .iter()
787                        .zip(&count)
788                        .map(|(&s, &c)| {
789                            s.checked_add(c - 1).ok_or_else(|| {
790                                FormatError::InvalidData(
791                                    "hyperslab box coordinate overflows".into(),
792                                )
793                            })
794                        })
795                        .collect::<FormatResult<Vec<u64>>>()?;
796                    Ok(HyperslabBlock { start, end })
797                })
798                .collect()
799        }
800    }
801}
802
803/// The version-2 REGULAR wire form: `(start, stride, count, block)` per
804/// dimension at 8 bytes each, which is the only encoding that can carry
805/// `H5S_UNLIMITED` at the default low format-version bound
806/// (`H5S__hyper_serialize`'s `version == 2` arm).
807fn encode_regular_hyperslab_v2(rank: usize, r: &RegularHyperslab) -> FormatResult<Vec<u8>> {
808    if r.start.len() != rank
809        || r.stride.len() != rank
810        || r.count.len() != rank
811        || r.block.len() != rank
812    {
813        return Err(FormatError::InvalidData(format!(
814            "regular hyperslab field length does not match rank {rank}"
815        )));
816    }
817    let mut buf = Vec::with_capacity(17 + rank * 32);
818    buf.extend_from_slice(&SEL_HYPERSLABS.to_le_bytes());
819    buf.extend_from_slice(&HYPER_VERSION_2.to_le_bytes());
820    buf.push(HYPER_REGULAR_FLAG);
821    // The length field version 2 keeps where version 3 keeps its encoded-size
822    // tag: the rank field plus the four 8-byte fields per dimension, exactly
823    // what `H5S__hyper_serialize` accumulates into `len`. `rank <= MAX_RANK`,
824    // so this cannot overflow.
825    let len = 4u32 + 32 * rank as u32;
826    buf.extend_from_slice(&len.to_le_bytes());
827    buf.extend_from_slice(&(rank as u32).to_le_bytes());
828    for d in 0..rank {
829        buf.extend_from_slice(&r.start[d].to_le_bytes());
830        buf.extend_from_slice(&r.stride[d].to_le_bytes());
831        buf.extend_from_slice(&r.count[d].to_le_bytes());
832        buf.extend_from_slice(&r.block[d].to_le_bytes());
833    }
834    Ok(buf)
835}
836
837fn encode_hyperslab(rank: usize, form: &Hyperslab) -> FormatResult<Vec<u8>> {
838    if rank == 0 || rank > MAX_RANK {
839        return Err(FormatError::InvalidData(format!(
840            "invalid hyperslab selection rank {rank}"
841        )));
842    }
843    // `H5S__hyper_get_version_enc_size` takes `MAX(H5S_HYPER_VERSION_2, ...)`
844    // the moment the selection has an unlimited dimension, whatever the low
845    // format-version bound: version 1 is a block list, and a list of blocks
846    // cannot spell an unbounded one. Version 2 fixes the encoded width at 8
847    // bytes, so `H5S_UNLIMITED` goes out as the all-ones field `read_dim`
848    // reads back.
849    if let Hyperslab::Regular(r) = form {
850        if r.unlim_dim().is_some() {
851            return encode_regular_hyperslab_v2(rank, r);
852        }
853    }
854    let blocks = hyperslab_to_block_list(rank, form)?;
855    let num_blocks: u32 = blocks.len().try_into().map_err(|_| {
856        FormatError::UnsupportedFeature(format!(
857            "hyperslab selection has {} blocks, too many for version-1 encode (u32 count)",
858            blocks.len()
859        ))
860    })?;
861    let rank_u32 = rank as u32;
862
863    let mut coords: Vec<u32> = Vec::with_capacity(blocks.len() * rank * 2);
864    for b in &blocks {
865        for &s in &b.start {
866            coords.push(to_u32(s)?);
867        }
868        for &e in &b.end {
869            coords.push(to_u32(e)?);
870        }
871    }
872
873    let block_payload = 8u32
874        .checked_mul(rank_u32)
875        .and_then(|v| v.checked_mul(num_blocks))
876        .ok_or_else(|| {
877            FormatError::UnsupportedFeature(
878                "hyperslab selection too large for version-1 encode".into(),
879            )
880        })?;
881    let len = 8u32.checked_add(block_payload).ok_or_else(|| {
882        FormatError::UnsupportedFeature("hyperslab selection too large for version-1 encode".into())
883    })?;
884
885    let mut buf = Vec::with_capacity(24 + coords.len() * 4);
886    buf.extend_from_slice(&SEL_HYPERSLABS.to_le_bytes());
887    buf.extend_from_slice(&HYPER_VERSION_1.to_le_bytes());
888    buf.extend_from_slice(&0u32.to_le_bytes()); // padding
889    buf.extend_from_slice(&len.to_le_bytes());
890    buf.extend_from_slice(&rank_u32.to_le_bytes());
891    buf.extend_from_slice(&num_blocks.to_le_bytes());
892    for v in coords {
893        buf.extend_from_slice(&v.to_le_bytes());
894    }
895    Ok(buf)
896}
897
898/// Cap on the number of boxes [`Selection::to_boxes`] will materialize for
899/// a REGULAR hyperslab (`count[0] * count[1] * ...`). `count` comes
900/// straight off the wire with no relation to buffer size the way a
901/// block-list's `num_blocks` is implicitly bounded by (each block consumes
902/// buffer bytes; a `(start, stride, count, block)` tuple does not), so an
903/// unchecked expansion could turn a few dozen bytes of crafted input into
904/// an unbounded allocation.
905const MAX_REGULAR_BOXES: u64 = 1 << 20;
906
907fn regular_hyperslab_to_boxes(r: &RegularHyperslab) -> FormatResult<Vec<(Vec<u64>, Vec<u64>)>> {
908    let rank = r.start.len();
909    if r.count.contains(&UNLIMITED) || r.block.contains(&UNLIMITED) {
910        return Err(FormatError::UnsupportedFeature(
911            "unlimited (H5S_UNLIMITED) regular hyperslab dimension".into(),
912        ));
913    }
914    if r.count.contains(&0) || r.block.contains(&0) {
915        return Ok(Vec::new());
916    }
917    let total_boxes = r
918        .count
919        .iter()
920        .try_fold(1u64, |acc, &c| acc.checked_mul(c))
921        .ok_or_else(|| FormatError::InvalidData("regular hyperslab box count overflows".into()))?;
922    if total_boxes > MAX_REGULAR_BOXES {
923        return Err(FormatError::UnsupportedFeature(format!(
924            "regular hyperslab selection expands to {total_boxes} boxes, over the \
925             {MAX_REGULAR_BOXES} cap"
926        )));
927    }
928
929    let mut boxes = Vec::with_capacity(total_boxes as usize);
930    let mut idx = vec![0u64; rank];
931    for _ in 0..total_boxes {
932        let start: Vec<u64> = (0..rank)
933            .map(|d| r.start[d] + idx[d] * r.stride[d])
934            .collect();
935        boxes.push((start, r.block.clone()));
936        for d in (0..rank).rev() {
937            idx[d] += 1;
938            if idx[d] < r.count[d] {
939                break;
940            }
941            idx[d] = 0;
942        }
943    }
944    Ok(boxes)
945}
946
947fn decode_all_none_body(buf: &[u8]) -> FormatResult<usize> {
948    if buf.len() < 4 + 8 {
949        return Err(FormatError::BufferTooShort {
950            needed: 4 + 8,
951            available: buf.len(),
952        });
953    }
954    let version = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
955    if version != ALL_NONE_VERSION {
956        return Err(FormatError::InvalidData(format!(
957            "bad version {version} for all/none dataspace selection"
958        )));
959    }
960    // 8 reserved bytes, unconditionally skipped (H5Sall.c / H5Snone.c).
961    Ok(4 + 8)
962}
963
964fn decode_hyperslab_body(buf: &[u8]) -> FormatResult<(usize, Hyperslab, usize)> {
965    let mut pos = 0usize;
966    if buf.len() < 4 {
967        return Err(FormatError::BufferTooShort {
968            needed: 4,
969            available: buf.len(),
970        });
971    }
972    let version = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
973    pos += 4;
974    if !(HYPER_VERSION_1..=HYPER_VERSION_3).contains(&version) {
975        return Err(FormatError::InvalidData(format!(
976            "bad version {version} for hyperslab dataspace selection"
977        )));
978    }
979
980    let mut flags = 0u8;
981    let enc_size: usize;
982    if version >= HYPER_VERSION_2 {
983        if buf.len() < pos + 1 {
984            return Err(FormatError::BufferTooShort {
985                needed: pos + 1,
986                available: buf.len(),
987            });
988        }
989        flags = buf[pos];
990        pos += 1;
991        if flags & !HYPER_REGULAR_FLAG != 0 {
992            return Err(FormatError::InvalidData(format!(
993                "unknown hyperslab selection flag bits in {flags:#x}"
994            )));
995        }
996
997        if version >= HYPER_VERSION_3 {
998            if buf.len() < pos + 1 {
999                return Err(FormatError::BufferTooShort {
1000                    needed: pos + 1,
1001                    available: buf.len(),
1002                });
1003            }
1004            enc_size = match buf[pos] {
1005                0x02 => 2,
1006                0x04 => 4,
1007                0x08 => 8,
1008                other => {
1009                    return Err(FormatError::InvalidData(format!(
1010                        "unknown hyperslab selection encoding size tag {other:#x}"
1011                    )))
1012                }
1013            };
1014            pos += 1;
1015        } else {
1016            // Version 2: 4 reserved bytes, encoding size fixed at 8.
1017            if buf.len() < pos + 4 {
1018                return Err(FormatError::BufferTooShort {
1019                    needed: pos + 4,
1020                    available: buf.len(),
1021                });
1022            }
1023            pos += 4;
1024            enc_size = 8;
1025        }
1026    } else {
1027        // Version 1: 8 reserved bytes, encoding size fixed at 4.
1028        if buf.len() < pos + 8 {
1029            return Err(FormatError::BufferTooShort {
1030                needed: pos + 8,
1031                available: buf.len(),
1032            });
1033        }
1034        pos += 8;
1035        enc_size = 4;
1036    }
1037
1038    if buf.len() < pos + 4 {
1039        return Err(FormatError::BufferTooShort {
1040            needed: pos + 4,
1041            available: buf.len(),
1042        });
1043    }
1044    let rank = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
1045    pos += 4;
1046    if rank == 0 || rank > MAX_RANK {
1047        return Err(FormatError::InvalidData(format!(
1048            "invalid hyperslab selection rank {rank}"
1049        )));
1050    }
1051
1052    if flags & HYPER_REGULAR_FLAG != 0 {
1053        let mut start = Vec::with_capacity(rank);
1054        let mut stride = Vec::with_capacity(rank);
1055        let mut count = Vec::with_capacity(rank);
1056        let mut block = Vec::with_capacity(rank);
1057        for _ in 0..rank {
1058            if buf.len() < pos + 4 * enc_size {
1059                return Err(FormatError::BufferTooShort {
1060                    needed: pos + 4 * enc_size,
1061                    available: buf.len(),
1062                });
1063            }
1064            start.push(read_plain(&buf[pos..], enc_size));
1065            pos += enc_size;
1066            stride.push(read_plain(&buf[pos..], enc_size));
1067            pos += enc_size;
1068            count.push(read_dim(&buf[pos..], enc_size));
1069            pos += enc_size;
1070            block.push(read_dim(&buf[pos..], enc_size));
1071            pos += enc_size;
1072        }
1073        Ok((
1074            rank,
1075            Hyperslab::Regular(RegularHyperslab {
1076                start,
1077                stride,
1078                count,
1079                block,
1080            }),
1081            pos,
1082        ))
1083    } else {
1084        if buf.len() < pos + enc_size {
1085            return Err(FormatError::BufferTooShort {
1086                needed: pos + enc_size,
1087                available: buf.len(),
1088            });
1089        }
1090        let num_blocks = read_plain(&buf[pos..], enc_size) as usize;
1091        pos += enc_size;
1092
1093        // No `Vec::with_capacity(num_blocks)`: `num_blocks` is untrusted
1094        // file data up to a 64-bit field, and the per-iteration buffer
1095        // check below already bounds the loop to at most
1096        // `buf.len() / (rank * 2 * enc_size)` iterations, so a corrupt
1097        // huge count fails with `BufferTooShort` instead of an
1098        // allocation blowup.
1099        let mut blocks = Vec::new();
1100        for _ in 0..num_blocks {
1101            let mut start = Vec::with_capacity(rank);
1102            let mut end = Vec::with_capacity(rank);
1103            if buf.len() < pos + 2 * rank * enc_size {
1104                return Err(FormatError::BufferTooShort {
1105                    needed: pos + 2 * rank * enc_size,
1106                    available: buf.len(),
1107                });
1108            }
1109            for _ in 0..rank {
1110                start.push(read_plain(&buf[pos..], enc_size));
1111                pos += enc_size;
1112            }
1113            for _ in 0..rank {
1114                end.push(read_plain(&buf[pos..], enc_size));
1115                pos += enc_size;
1116            }
1117            blocks.push(HyperslabBlock { start, end });
1118        }
1119        Ok((rank, Hyperslab::Blocks(blocks), pos))
1120    }
1121}
1122
1123fn decode_points_body(buf: &[u8]) -> FormatResult<(PointSelection, usize)> {
1124    let mut pos = 0usize;
1125    if buf.len() < 4 {
1126        return Err(FormatError::BufferTooShort {
1127            needed: 4,
1128            available: buf.len(),
1129        });
1130    }
1131    let version = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
1132    pos += 4;
1133    if version != POINT_VERSION_1 && version != POINT_VERSION_2 {
1134        return Err(FormatError::InvalidData(format!(
1135            "bad version {version} for point dataspace selection"
1136        )));
1137    }
1138
1139    let enc_size: usize;
1140    if version >= POINT_VERSION_2 {
1141        if buf.len() < pos + 1 {
1142            return Err(FormatError::BufferTooShort {
1143                needed: pos + 1,
1144                available: buf.len(),
1145            });
1146        }
1147        enc_size = match buf[pos] {
1148            0x02 => 2,
1149            0x04 => 4,
1150            0x08 => 8,
1151            other => {
1152                return Err(FormatError::InvalidData(format!(
1153                    "unknown point selection encoding size tag {other:#x}"
1154                )))
1155            }
1156        };
1157        pos += 1;
1158    } else {
1159        // Version 1: 4 padding bytes + a 4-byte length field, neither of
1160        // which this decoder validates (H5S__point_deserialize doesn't
1161        // either — the length is a serialize-side convenience, not a
1162        // decode-side check). Encoding size is fixed at 4.
1163        if buf.len() < pos + 8 {
1164            return Err(FormatError::BufferTooShort {
1165                needed: pos + 8,
1166                available: buf.len(),
1167            });
1168        }
1169        pos += 8;
1170        enc_size = 4;
1171    }
1172
1173    if buf.len() < pos + 4 {
1174        return Err(FormatError::BufferTooShort {
1175            needed: pos + 4,
1176            available: buf.len(),
1177        });
1178    }
1179    let rank = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
1180    pos += 4;
1181    if rank == 0 || rank > MAX_RANK {
1182        return Err(FormatError::InvalidData(format!(
1183            "invalid point selection rank {rank}"
1184        )));
1185    }
1186
1187    if buf.len() < pos + enc_size {
1188        return Err(FormatError::BufferTooShort {
1189            needed: pos + enc_size,
1190            available: buf.len(),
1191        });
1192    }
1193    let num_points = read_plain(&buf[pos..], enc_size);
1194    pos += enc_size;
1195
1196    // Mirrors H5S__point_deserialize's own overflow guard: `rank *
1197    // enc_size * num_points` is computed in checked 64-bit arithmetic
1198    // before it is trusted as a buffer offset, so a crafted huge
1199    // `num_points` fails cleanly here instead of via an under-allocated
1200    // `Vec::with_capacity`.
1201    let point_bytes = (rank as u64)
1202        .checked_mul(enc_size as u64)
1203        .and_then(|per_point| per_point.checked_mul(num_points))
1204        .ok_or_else(|| {
1205            FormatError::InvalidData("point selection coordinate buffer size overflows".into())
1206        })?;
1207    if (buf.len() as u64) < pos as u64 + point_bytes {
1208        let needed = usize::try_from(point_bytes)
1209            .ok()
1210            .and_then(|b| pos.checked_add(b))
1211            .unwrap_or(usize::MAX);
1212        return Err(FormatError::BufferTooShort {
1213            needed,
1214            available: buf.len(),
1215        });
1216    }
1217
1218    let mut points = Vec::with_capacity(num_points as usize);
1219    for _ in 0..num_points {
1220        let mut coord = Vec::with_capacity(rank);
1221        for _ in 0..rank {
1222            coord.push(read_plain(&buf[pos..], enc_size));
1223            pos += enc_size;
1224        }
1225        points.push(coord);
1226    }
1227
1228    Ok((PointSelection { rank, points }, pos))
1229}
1230
1231/// Decode an `n`-byte little-endian field with no sentinel substitution
1232/// (`start`, `stride`, and block-list coordinates).
1233fn read_plain(buf: &[u8], n: usize) -> u64 {
1234    crate::format::bytes::read_le_uint(buf, n)
1235}
1236
1237/// Decode an `n`-byte little-endian `count` or `block` field, mapping an
1238/// all-ones field of that width to [`UNLIMITED`] — `H5S_UINT16_MAX` /
1239/// `H5S_UINT32_MAX` / `H5S_UINT64_MAX` in H5Shyper.c, one sentinel per
1240/// encoding width since a narrower field cannot spell `HSIZE_UNDEF`
1241/// itself.
1242fn read_dim(buf: &[u8], n: usize) -> u64 {
1243    let v = read_plain(buf, n);
1244    let all_ones = if n >= 8 {
1245        u64::MAX
1246    } else {
1247        (1u64 << (n * 8)) - 1
1248    };
1249    if v == all_ones {
1250        UNLIMITED
1251    } else {
1252        v
1253    }
1254}
1255
1256// ======================================================================= tests
1257
1258#[cfg(test)]
1259mod tests {
1260    use super::*;
1261
1262    /// Strip an `H5Sencode2` envelope (`tests/fixtures/gen_selection_images.c`
1263    /// captures the whole blob) down to the selection-only bytes this
1264    /// module decodes: `type(1) + version(1) + sizeof_size(1) +
1265    /// extent_size(4 LE)` followed by `extent_size` bytes of serialized
1266    /// extent (H5S.c `H5S_encode`), then the selection.
1267    fn strip_h5sencode_envelope(blob: &[u8]) -> &[u8] {
1268        let extent_size = u32::from_le_bytes([blob[3], blob[4], blob[5], blob[6]]) as usize;
1269        &blob[7 + extent_size..]
1270    }
1271
1272    /// Byte-for-byte the source selection h5debug reported inside a real
1273    /// h5py-written VDS mapping entry (`layout[...] = VirtualSource(...)`,
1274    /// full-extent mapping): H5S_SEL_ALL, version 1, 8 reserved bytes.
1275    #[test]
1276    fn decode_all_selection() {
1277        let buf = [
1278            0x03, 0x00, 0x00, 0x00, // type = SEL_ALL
1279            0x01, 0x00, 0x00, 0x00, // version = 1
1280            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
1281        ];
1282        let (sel, consumed) = Selection::decode(&buf).unwrap();
1283        assert_eq!(consumed, buf.len());
1284        assert_eq!(sel, Selection::All);
1285    }
1286
1287    #[test]
1288    fn decode_none_selection() {
1289        let buf = [
1290            0x00, 0x00, 0x00, 0x00, // type = SEL_NONE
1291            0x01, 0x00, 0x00, 0x00, // version = 1
1292            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
1293        ];
1294        let (sel, consumed) = Selection::decode(&buf).unwrap();
1295        assert_eq!(consumed, buf.len());
1296        assert_eq!(sel, Selection::None);
1297    }
1298
1299    /// A trailing selection right after this one must be left untouched —
1300    /// a VDS mapping entry decodes a source selection immediately
1301    /// followed by a virtual selection out of one shared buffer.
1302    #[test]
1303    fn decode_all_selection_leaves_trailer_untouched() {
1304        let mut buf = vec![0x03, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1305        buf.extend_from_slice(&[0xAA; 4]);
1306        let (sel, consumed) = Selection::decode(&buf).unwrap();
1307        assert_eq!(sel, Selection::All);
1308        assert_eq!(consumed, 16);
1309        assert_eq!(&buf[consumed..], &[0xAA; 4]);
1310    }
1311
1312    /// Byte-for-byte a real h5py-written VDS mapping's virtual selection
1313    /// for `layout[4:12] = VirtualSource(..., shape=(8,))`: hyperslab,
1314    /// version 1 (so no flags byte — always the block-list form), rank 1,
1315    /// one block spanning element 4 through 11 inclusive.
1316    #[test]
1317    fn h5py_single_block_selection_is_version_one() {
1318        let mut buf = vec![0x02, 0, 0, 0]; // type = SEL_HYPERSLABS
1319        buf.extend_from_slice(&1u32.to_le_bytes()); // version = 1
1320        buf.extend_from_slice(&[0u8; 8]); // reserved
1321        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1322        buf.extend_from_slice(&1u32.to_le_bytes()); // num_blocks = 1
1323        buf.extend_from_slice(&4u32.to_le_bytes()); // block 0 start
1324        buf.extend_from_slice(&11u32.to_le_bytes()); // block 0 end (inclusive)
1325
1326        let (sel, consumed) = Selection::decode(&buf).unwrap();
1327        assert_eq!(consumed, buf.len());
1328        match sel {
1329            Selection::Hyperslab {
1330                rank,
1331                form: Hyperslab::Blocks(blocks),
1332            } => {
1333                assert_eq!(rank, 1);
1334                assert_eq!(
1335                    blocks,
1336                    vec![HyperslabBlock {
1337                        start: vec![4],
1338                        end: vec![11],
1339                    }]
1340                );
1341            }
1342            other => panic!("expected a version-1 block list, got {other:?}"),
1343        }
1344    }
1345
1346    #[test]
1347    fn decode_hyperslab_block_list_multi_block_2d() {
1348        let mut buf = vec![0x02, 0, 0, 0]; // SEL_HYPERSLABS
1349        buf.extend_from_slice(&1u32.to_le_bytes()); // version 1
1350        buf.extend_from_slice(&[0u8; 8]);
1351        buf.extend_from_slice(&2u32.to_le_bytes()); // rank = 2
1352        buf.extend_from_slice(&2u32.to_le_bytes()); // num_blocks = 2
1353                                                    // block 0: (0,0)..(1,1)
1354        for v in [0u32, 0, 1, 1] {
1355            buf.extend_from_slice(&v.to_le_bytes());
1356        }
1357        // block 1: (2,2)..(3,3)
1358        for v in [2u32, 2, 3, 3] {
1359            buf.extend_from_slice(&v.to_le_bytes());
1360        }
1361
1362        let (sel, consumed) = Selection::decode(&buf).unwrap();
1363        assert_eq!(consumed, buf.len());
1364        match sel {
1365            Selection::Hyperslab {
1366                rank,
1367                form: Hyperslab::Blocks(blocks),
1368            } => {
1369                assert_eq!(rank, 2);
1370                assert_eq!(blocks.len(), 2);
1371                assert_eq!(blocks[0].start, vec![0, 0]);
1372                assert_eq!(blocks[0].end, vec![1, 1]);
1373                assert_eq!(blocks[1].start, vec![2, 2]);
1374                assert_eq!(blocks[1].end, vec![3, 3]);
1375            }
1376            other => panic!("expected block list, got {other:?}"),
1377        }
1378    }
1379
1380    /// A version-2, REGULAR-flagged hyperslab: start/stride/count/block per
1381    /// dimension, 8-byte fields (the version-2 default when no explicit
1382    /// encoding-size byte is present).
1383    #[test]
1384    fn decode_regular_hyperslab_v2() {
1385        let mut buf = vec![0x02, 0, 0, 0]; // SEL_HYPERSLABS
1386        buf.extend_from_slice(&2u32.to_le_bytes()); // version 2
1387        buf.push(0x01); // flags = REGULAR
1388        buf.extend_from_slice(&[0u8; 4]); // reserved
1389        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1390        buf.extend_from_slice(&2u64.to_le_bytes()); // start
1391        buf.extend_from_slice(&4u64.to_le_bytes()); // stride
1392        buf.extend_from_slice(&3u64.to_le_bytes()); // count
1393        buf.extend_from_slice(&2u64.to_le_bytes()); // block
1394
1395        let (sel, consumed) = Selection::decode(&buf).unwrap();
1396        assert_eq!(consumed, buf.len());
1397        match sel {
1398            Selection::Hyperslab {
1399                rank,
1400                form: Hyperslab::Regular(r),
1401            } => {
1402                assert_eq!(rank, 1);
1403                assert_eq!(r.start, vec![2]);
1404                assert_eq!(r.stride, vec![4]);
1405                assert_eq!(r.count, vec![3]);
1406                assert_eq!(r.block, vec![2]);
1407            }
1408            other => panic!("expected a regular hyperslab, got {other:?}"),
1409        }
1410    }
1411
1412    /// A version-3, REGULAR-flagged hyperslab with an explicit 2-byte
1413    /// encoding size and an unlimited count — the all-ones sentinel for
1414    /// that width must decode to [`UNLIMITED`], not `0xFFFF` taken
1415    /// literally.
1416    #[test]
1417    fn decode_regular_hyperslab_v3_unlimited_count() {
1418        let mut buf = vec![0x02, 0, 0, 0]; // SEL_HYPERSLABS
1419        buf.extend_from_slice(&3u32.to_le_bytes()); // version 3
1420        buf.push(0x01); // flags = REGULAR
1421        buf.push(0x02); // enc_size tag = 2 bytes
1422        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1423        buf.extend_from_slice(&0u16.to_le_bytes()); // start = 0
1424        buf.extend_from_slice(&5u16.to_le_bytes()); // stride = 5
1425        buf.extend_from_slice(&0xFFFFu16.to_le_bytes()); // count = UNLIMITED
1426        buf.extend_from_slice(&3u16.to_le_bytes()); // block = 3
1427
1428        let (sel, consumed) = Selection::decode(&buf).unwrap();
1429        assert_eq!(consumed, buf.len());
1430        match sel {
1431            Selection::Hyperslab {
1432                form: Hyperslab::Regular(r),
1433                ..
1434            } => {
1435                assert_eq!(r.count, vec![UNLIMITED]);
1436                assert_eq!(r.block, vec![3]);
1437            }
1438            other => panic!("expected a regular hyperslab, got {other:?}"),
1439        }
1440    }
1441
1442    #[test]
1443    fn decode_points_truncated_header_is_buffer_too_short() {
1444        let buf = [0x01, 0, 0, 0]; // type = SEL_POINTS, no body at all
1445        let err = Selection::decode(&buf).unwrap_err();
1446        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1447    }
1448
1449    /// Byte-for-byte a libhdf5-captured `H5Sencode2` image
1450    /// (`tests/fixtures/gen_selection_images.c`, `points4_v1.bin`): a
1451    /// version-1 point selection, rank 1, points at 1/3/7/15.
1452    #[test]
1453    fn decode_points_matches_libhdf5_image() {
1454        let blob = include_bytes!("../../tests/fixtures/points4_v1.bin");
1455        let sel_bytes = strip_h5sencode_envelope(blob);
1456        let (sel, consumed) = Selection::decode(sel_bytes).unwrap();
1457        assert_eq!(consumed, sel_bytes.len());
1458        assert_eq!(
1459            sel,
1460            Selection::Points(PointSelection {
1461                rank: 1,
1462                points: vec![vec![1], vec![3], vec![7], vec![15]],
1463            })
1464        );
1465    }
1466
1467    #[test]
1468    fn decode_points_version_2_small_enc_size() {
1469        let mut buf = vec![0x01, 0, 0, 0]; // SEL_POINTS
1470        buf.extend_from_slice(&2u32.to_le_bytes()); // version 2
1471        buf.push(0x02); // enc_size tag = 2 bytes
1472        buf.extend_from_slice(&2u32.to_le_bytes()); // rank = 2
1473        buf.extend_from_slice(&2u16.to_le_bytes()); // num_points = 2
1474        buf.extend_from_slice(&1u16.to_le_bytes()); // point 0 = (1, 5)
1475        buf.extend_from_slice(&5u16.to_le_bytes());
1476        buf.extend_from_slice(&9u16.to_le_bytes()); // point 1 = (9, 0)
1477        buf.extend_from_slice(&0u16.to_le_bytes());
1478
1479        let (sel, consumed) = Selection::decode(&buf).unwrap();
1480        assert_eq!(consumed, buf.len());
1481        assert_eq!(
1482            sel,
1483            Selection::Points(PointSelection {
1484                rank: 2,
1485                points: vec![vec![1, 5], vec![9, 0]],
1486            })
1487        );
1488    }
1489
1490    #[test]
1491    fn decode_points_rejects_bad_version() {
1492        let mut buf = vec![0x01, 0, 0, 0];
1493        buf.extend_from_slice(&3u32.to_le_bytes()); // version 3 does not exist
1494        let err = Selection::decode(&buf).unwrap_err();
1495        assert!(matches!(err, FormatError::InvalidData(_)));
1496    }
1497
1498    #[test]
1499    fn decode_points_rejects_unknown_enc_size_tag() {
1500        let mut buf = vec![0x01, 0, 0, 0];
1501        buf.extend_from_slice(&2u32.to_le_bytes()); // version 2
1502        buf.push(0x03); // not one of 2/4/8
1503        let err = Selection::decode(&buf).unwrap_err();
1504        assert!(matches!(err, FormatError::InvalidData(_)));
1505    }
1506
1507    #[test]
1508    fn decode_points_rejects_zero_rank() {
1509        let mut buf = vec![0x01, 0, 0, 0];
1510        buf.extend_from_slice(&1u32.to_le_bytes());
1511        buf.extend_from_slice(&[0u8; 8]);
1512        buf.extend_from_slice(&0u32.to_le_bytes()); // rank = 0
1513        let err = Selection::decode(&buf).unwrap_err();
1514        assert!(matches!(err, FormatError::InvalidData(_)));
1515    }
1516
1517    #[test]
1518    fn decode_points_rejects_rank_over_max() {
1519        let mut buf = vec![0x01, 0, 0, 0];
1520        buf.extend_from_slice(&1u32.to_le_bytes());
1521        buf.extend_from_slice(&[0u8; 8]);
1522        buf.extend_from_slice(&33u32.to_le_bytes()); // rank = 33 > 32
1523        let err = Selection::decode(&buf).unwrap_err();
1524        assert!(matches!(err, FormatError::InvalidData(_)));
1525    }
1526
1527    #[test]
1528    fn decode_points_truncated_coordinates() {
1529        // Claims 5 points of rank 1 but the buffer only has room for one.
1530        let mut buf = vec![0x01, 0, 0, 0];
1531        buf.extend_from_slice(&1u32.to_le_bytes()); // version 1
1532        buf.extend_from_slice(&[0u8; 8]); // padding + length placeholder
1533        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1534        buf.extend_from_slice(&5u32.to_le_bytes()); // num_points = 5 (lie)
1535        buf.extend_from_slice(&0u32.to_le_bytes());
1536        let err = Selection::decode(&buf).unwrap_err();
1537        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1538    }
1539
1540    /// A `num_points` claim that comfortably fits the `rank * enc_size *
1541    /// num_points` multiplication (so it doesn't hit the overflow guard)
1542    /// but wildly exceeds the actual buffer must still fail on the
1543    /// bounds check rather than attempting to allocate.
1544    #[test]
1545    fn decode_points_huge_num_points_claim_does_not_allocate() {
1546        let mut buf = vec![0x01, 0, 0, 0];
1547        buf.extend_from_slice(&2u32.to_le_bytes()); // version 2
1548        buf.push(0x08); // enc_size tag = 8 bytes
1549        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1550        buf.extend_from_slice(&(1u64 << 40).to_le_bytes()); // num_points (lie)
1551        let err = Selection::decode(&buf).unwrap_err();
1552        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1553    }
1554
1555    /// A `num_points` claim that overflows the `rank * enc_size *
1556    /// num_points` multiplication itself must fail cleanly rather than
1557    /// wrapping into a small, incorrect buffer requirement.
1558    #[test]
1559    fn decode_points_num_points_overflow_does_not_allocate() {
1560        let mut buf = vec![0x01, 0, 0, 0];
1561        buf.extend_from_slice(&2u32.to_le_bytes()); // version 2
1562        buf.push(0x08); // enc_size tag = 8 bytes
1563        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1564        buf.extend_from_slice(&(u64::MAX - 1).to_le_bytes()); // num_points (lie)
1565        let err = Selection::decode(&buf).unwrap_err();
1566        assert!(matches!(err, FormatError::InvalidData(_)));
1567    }
1568
1569    #[test]
1570    fn to_boxes_points_each_point_is_its_own_unit_box() {
1571        let sel = Selection::Points(PointSelection {
1572            rank: 2,
1573            points: vec![vec![1, 2], vec![5, 5]],
1574        });
1575        let boxes = sel.to_boxes(&[8, 8]).unwrap();
1576        assert_eq!(
1577            boxes,
1578            vec![(vec![1, 2], vec![1, 1]), (vec![5, 5], vec![1, 1])]
1579        );
1580    }
1581
1582    #[test]
1583    fn to_boxes_points_rejects_rank_mismatch() {
1584        let sel = Selection::Points(PointSelection {
1585            rank: 2,
1586            points: vec![],
1587        });
1588        let err = sel.to_boxes(&[8]).unwrap_err();
1589        assert!(matches!(err, FormatError::InvalidData(_)));
1590    }
1591
1592    #[test]
1593    fn decode_unknown_type_is_invalid() {
1594        let buf = [0x09, 0, 0, 0];
1595        let err = Selection::decode(&buf).unwrap_err();
1596        assert!(matches!(err, FormatError::InvalidData(_)));
1597    }
1598
1599    #[test]
1600    fn decode_bad_all_version() {
1601        let buf = [0x03, 0, 0, 0, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1602        let err = Selection::decode(&buf).unwrap_err();
1603        assert!(matches!(err, FormatError::InvalidData(_)));
1604    }
1605
1606    #[test]
1607    fn decode_bad_hyperslab_version() {
1608        let buf = [0x02, 0, 0, 0, 0x04, 0, 0, 0];
1609        let err = Selection::decode(&buf).unwrap_err();
1610        assert!(matches!(err, FormatError::InvalidData(_)));
1611    }
1612
1613    #[test]
1614    fn decode_hyperslab_rejects_unknown_flag_bits() {
1615        let mut buf = vec![0x02, 0, 0, 0];
1616        buf.extend_from_slice(&2u32.to_le_bytes());
1617        buf.push(0x02); // bit 1 is not a known flag
1618        buf.extend_from_slice(&[0u8; 4]);
1619        buf.extend_from_slice(&1u32.to_le_bytes());
1620        let err = Selection::decode(&buf).unwrap_err();
1621        assert!(matches!(err, FormatError::InvalidData(_)));
1622    }
1623
1624    #[test]
1625    fn decode_hyperslab_rejects_zero_rank() {
1626        let mut buf = vec![0x02, 0, 0, 0];
1627        buf.extend_from_slice(&1u32.to_le_bytes());
1628        buf.extend_from_slice(&[0u8; 8]);
1629        buf.extend_from_slice(&0u32.to_le_bytes()); // rank = 0
1630        let err = Selection::decode(&buf).unwrap_err();
1631        assert!(matches!(err, FormatError::InvalidData(_)));
1632    }
1633
1634    #[test]
1635    fn decode_hyperslab_rejects_rank_over_max() {
1636        let mut buf = vec![0x02, 0, 0, 0];
1637        buf.extend_from_slice(&1u32.to_le_bytes());
1638        buf.extend_from_slice(&[0u8; 8]);
1639        buf.extend_from_slice(&33u32.to_le_bytes()); // rank = 33 > 32
1640        let err = Selection::decode(&buf).unwrap_err();
1641        assert!(matches!(err, FormatError::InvalidData(_)));
1642    }
1643
1644    #[test]
1645    fn decode_truncated_header() {
1646        let buf = [0x03, 0, 0];
1647        let err = Selection::decode(&buf).unwrap_err();
1648        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1649    }
1650
1651    #[test]
1652    fn decode_truncated_hyperslab_blocks() {
1653        // Claims 5 blocks of rank 1 but the buffer only has room for one.
1654        let mut buf = vec![0x02, 0, 0, 0];
1655        buf.extend_from_slice(&1u32.to_le_bytes());
1656        buf.extend_from_slice(&[0u8; 8]);
1657        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1658        buf.extend_from_slice(&5u32.to_le_bytes()); // num_blocks = 5 (lie)
1659        buf.extend_from_slice(&0u32.to_le_bytes());
1660        buf.extend_from_slice(&1u32.to_le_bytes());
1661        let err = Selection::decode(&buf).unwrap_err();
1662        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1663    }
1664
1665    /// A `num_blocks` claim near `u64::MAX` (the widest on-disk field, via
1666    /// an 8-byte encoding size) must fail on the first bounds check rather
1667    /// than attempting to allocate — this is the case
1668    /// `Vec::with_capacity(num_blocks)` would have been unsafe for.
1669    #[test]
1670    fn decode_huge_num_blocks_claim_does_not_allocate() {
1671        let mut buf = vec![0x02, 0, 0, 0];
1672        buf.extend_from_slice(&3u32.to_le_bytes()); // version 3
1673        buf.push(0x00); // flags = 0 (block list)
1674        buf.push(0x08); // enc_size tag = 8 bytes
1675        buf.extend_from_slice(&1u32.to_le_bytes()); // rank = 1
1676        buf.extend_from_slice(&(u64::MAX - 1).to_le_bytes()); // num_blocks (lie)
1677        let err = Selection::decode(&buf).unwrap_err();
1678        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1679    }
1680
1681    // --------------------------------------------------------------- to_boxes
1682
1683    /// A multi-block selection bounds to the box that covers every block, the
1684    /// same answer `H5Sget_select_bounds` gives. Moved here with the decoder
1685    /// it belongs to, from `format::reference`'s own copy.
1686    #[test]
1687    fn bounds_cover_every_block_and_point() {
1688        let hyper = Selection::Hyperslab {
1689            rank: 2,
1690            form: Hyperslab::Blocks(vec![
1691                HyperslabBlock {
1692                    start: vec![4, 1],
1693                    end: vec![5, 3],
1694                },
1695                HyperslabBlock {
1696                    start: vec![0, 6],
1697                    end: vec![1, 7],
1698                },
1699            ]),
1700        };
1701        assert_eq!(hyper.bounds(), Some((vec![0, 1], vec![5, 7])));
1702        let points = Selection::Points(PointSelection {
1703            rank: 2,
1704            points: vec![vec![3, 9], vec![7, 2]],
1705        });
1706        assert_eq!(points.bounds(), Some((vec![3, 2], vec![7, 9])));
1707        assert_eq!(Selection::All.bounds(), None);
1708        assert_eq!(Selection::None.bounds(), None);
1709    }
1710
1711    /// A regular hyperslab lists no blocks; it stores the
1712    /// start/stride/count/block pattern, and its bounds cover the blocks that
1713    /// pattern expands to.
1714    #[test]
1715    fn bounds_of_a_regular_hyperslab_cover_its_expansion() {
1716        let mut buf = Vec::new();
1717        buf.extend_from_slice(&SEL_HYPERSLABS.to_le_bytes());
1718        buf.extend_from_slice(&3u32.to_le_bytes());
1719        buf.push(HYPER_REGULAR_FLAG);
1720        buf.push(4); // coordinate width
1721        buf.extend_from_slice(&1u32.to_le_bytes()); // rank
1722        for v in [2u32, 5, 3, 2] {
1723            // start 2, stride 5, count 3, block 2
1724            buf.extend_from_slice(&v.to_le_bytes());
1725        }
1726        let (selection, consumed) = Selection::decode(&buf).unwrap();
1727        assert_eq!(consumed, buf.len());
1728        // Blocks [2,3], [7,8], [12,13].
1729        assert_eq!(selection.bounds(), Some((vec![2], vec![13])));
1730    }
1731
1732    /// A regular hyperslab with no upper bound has no bounding box either,
1733    /// rather than one built from the `UNLIMITED` sentinel.
1734    #[test]
1735    fn bounds_of_an_unlimited_regular_hyperslab_are_absent() {
1736        let selection = Selection::Hyperslab {
1737            rank: 1,
1738            form: Hyperslab::Regular(RegularHyperslab {
1739                start: vec![0],
1740                stride: vec![1],
1741                count: vec![UNLIMITED],
1742                block: vec![1],
1743            }),
1744        };
1745        assert_eq!(selection.bounds(), None);
1746    }
1747
1748    #[test]
1749    fn to_boxes_all_covers_the_full_extent() {
1750        let boxes = Selection::All.to_boxes(&[3, 5]).unwrap();
1751        assert_eq!(boxes, vec![(vec![0, 0], vec![3, 5])]);
1752    }
1753
1754    #[test]
1755    fn to_boxes_none_is_empty() {
1756        assert_eq!(Selection::None.to_boxes(&[3, 5]).unwrap(), vec![]);
1757    }
1758
1759    #[test]
1760    fn to_boxes_single_block_matches_h5py_fixture() {
1761        // layout[4:12] = VirtualSource(..., shape=(8,)): one block, [4, 11].
1762        let sel = Selection::Hyperslab {
1763            rank: 1,
1764            form: Hyperslab::Blocks(vec![HyperslabBlock {
1765                start: vec![4],
1766                end: vec![11],
1767            }]),
1768        };
1769        let boxes = sel.to_boxes(&[20]).unwrap();
1770        assert_eq!(boxes, vec![(vec![4], vec![8])]);
1771    }
1772
1773    #[test]
1774    fn to_boxes_multi_block_2d() {
1775        let sel = Selection::Hyperslab {
1776            rank: 2,
1777            form: Hyperslab::Blocks(vec![
1778                HyperslabBlock {
1779                    start: vec![0, 0],
1780                    end: vec![1, 1],
1781                },
1782                HyperslabBlock {
1783                    start: vec![2, 2],
1784                    end: vec![3, 3],
1785                },
1786            ]),
1787        };
1788        let boxes = sel.to_boxes(&[4, 4]).unwrap();
1789        assert_eq!(
1790            boxes,
1791            vec![(vec![0, 0], vec![2, 2]), (vec![2, 2], vec![2, 2])]
1792        );
1793    }
1794
1795    /// Runs come out in H5S element order, which for a multi-block
1796    /// hyperslab is *not* the order the boxes come out in: the 2x2-block
1797    /// grid over a 4x4 extent decomposes into boxes
1798    /// `(0,0) (0,2) (2,0) (2,2)`, but the elements are visited row by row,
1799    /// so the run list is `(0,0) (0,2) (1,0) (1,2) (2,0) ...`.
1800    #[test]
1801    fn resolve_orders_runs_by_element_not_by_box() {
1802        let sel = Selection::Hyperslab {
1803            rank: 2,
1804            form: Hyperslab::Regular(RegularHyperslab {
1805                start: vec![0, 0],
1806                stride: vec![2, 2],
1807                count: vec![2, 2],
1808                block: vec![2, 2],
1809            }),
1810        };
1811        let resolved = sel.resolve(&[4, 4]).unwrap();
1812        assert_eq!(resolved.n_elements(), 16);
1813        // Every element of the 4x4 extent, once, in row-major order.
1814        let mut covered = Vec::new();
1815        for r in &resolved.runs {
1816            for k in 0..r.len {
1817                covered.push(r.offset_in_extent + k);
1818            }
1819        }
1820        assert_eq!(covered, (0..16).collect::<Vec<u64>>());
1821        // The first two runs are the two halves of row 0, taken from
1822        // different boxes.
1823        assert_eq!(resolved.runs[0].box_index, 0);
1824        assert_eq!(resolved.runs[1].box_index, 1);
1825        assert_eq!(resolved.runs[0].len, 2);
1826        // Row 1 of box 0 sits two elements into that box's own buffer.
1827        assert_eq!(resolved.runs[2].box_index, 0);
1828        assert_eq!(resolved.runs[2].offset_in_box, 2);
1829    }
1830
1831    /// A box that covers a whole trailing region is contiguous in both the
1832    /// box and the extent, so it collapses to one run rather than one per
1833    /// row — the same merge a dual-array walk performs.
1834    #[test]
1835    fn resolve_coalesces_a_box_that_fills_its_extent() {
1836        let resolved = Selection::All.resolve(&[3, 4]).unwrap();
1837        assert_eq!(resolved.runs.len(), 1);
1838        assert_eq!(resolved.runs[0].len, 12);
1839        assert_eq!(resolved.n_elements(), 12);
1840    }
1841
1842    /// A partial box cannot coalesce across its second dimension: rows are
1843    /// separated by the extent's stride.
1844    #[test]
1845    fn resolve_keeps_one_run_per_row_of_a_partial_box() {
1846        let sel = Selection::Hyperslab {
1847            rank: 2,
1848            form: Hyperslab::Blocks(vec![HyperslabBlock {
1849                start: vec![1, 1],
1850                end: vec![2, 2],
1851            }]),
1852        };
1853        let resolved = sel.resolve(&[4, 4]).unwrap();
1854        assert_eq!(resolved.runs.len(), 2);
1855        assert_eq!(resolved.runs[0].offset_in_extent, 5);
1856        assert_eq!(resolved.runs[0].offset_in_box, 0);
1857        assert_eq!(resolved.runs[1].offset_in_extent, 9);
1858        assert_eq!(resolved.runs[1].offset_in_box, 2);
1859    }
1860
1861    /// Selection order for a point list is the order the coordinates were
1862    /// given in (`H5S__point_iter_next`, H5Spoint.c), not coordinate order,
1863    /// so the runs must not be sorted.
1864    #[test]
1865    fn resolve_keeps_point_selection_order() {
1866        let sel = Selection::Points(PointSelection {
1867            rank: 2,
1868            points: vec![vec![2, 3], vec![0, 1], vec![1, 0]],
1869        });
1870        let resolved = sel.resolve(&[4, 4]).unwrap();
1871        assert_eq!(
1872            resolved
1873                .runs
1874                .iter()
1875                .map(|r| r.offset_in_extent)
1876                .collect::<Vec<u64>>(),
1877            vec![11, 1, 4]
1878        );
1879        assert!(resolved.runs.iter().all(|r| r.len == 1));
1880    }
1881
1882    /// `to_boxes` deliberately does not clip a block against the extent, so
1883    /// `resolve` — which needs every box's offset within that extent to mean
1884    /// something — is the caller that rejects one reaching past it.
1885    #[test]
1886    fn resolve_rejects_a_box_past_the_extent() {
1887        let sel = Selection::Hyperslab {
1888            rank: 1,
1889            form: Hyperslab::Blocks(vec![HyperslabBlock {
1890                start: vec![2],
1891                end: vec![5],
1892            }]),
1893        };
1894        let err = sel.resolve(&[4]).unwrap_err();
1895        assert!(
1896            format!("{err}").contains("past the extent"),
1897            "unexpected error: {err}"
1898        );
1899    }
1900
1901    /// A scalar dataspace holds one element and takes only all/none.
1902    #[test]
1903    fn resolve_of_a_scalar_extent_holds_one_element() {
1904        let all = Selection::All.resolve(&[]).unwrap();
1905        assert_eq!(all.n_elements(), 1);
1906        assert_eq!(all.runs[0].len, 1);
1907        assert_eq!(Selection::None.resolve(&[]).unwrap().n_elements(), 0);
1908    }
1909
1910    #[test]
1911    fn to_boxes_rejects_rank_mismatch() {
1912        let sel = Selection::Hyperslab {
1913            rank: 2,
1914            form: Hyperslab::Blocks(vec![]),
1915        };
1916        let err = sel.to_boxes(&[4]).unwrap_err();
1917        assert!(matches!(err, FormatError::InvalidData(_)));
1918    }
1919
1920    #[test]
1921    fn to_boxes_rejects_inverted_block() {
1922        let sel = Selection::Hyperslab {
1923            rank: 1,
1924            form: Hyperslab::Blocks(vec![HyperslabBlock {
1925                start: vec![5],
1926                end: vec![2],
1927            }]),
1928        };
1929        let err = sel.to_boxes(&[10]).unwrap_err();
1930        assert!(matches!(err, FormatError::InvalidData(_)));
1931    }
1932
1933    /// A regular hyperslab expands into one box per `count` position, block
1934    /// sized `block`, spaced `stride` apart — the odometer must walk the
1935    /// last dimension fastest, matching row-major box order.
1936    #[test]
1937    fn to_boxes_regular_expands_2d_grid() {
1938        let sel = Selection::Hyperslab {
1939            rank: 2,
1940            form: Hyperslab::Regular(RegularHyperslab {
1941                start: vec![0, 0],
1942                stride: vec![4, 4],
1943                count: vec![2, 2],
1944                block: vec![2, 2],
1945            }),
1946        };
1947        let boxes = sel.to_boxes(&[8, 8]).unwrap();
1948        assert_eq!(
1949            boxes,
1950            vec![
1951                (vec![0, 0], vec![2, 2]),
1952                (vec![0, 4], vec![2, 2]),
1953                (vec![4, 0], vec![2, 2]),
1954                (vec![4, 4], vec![2, 2]),
1955            ]
1956        );
1957    }
1958
1959    #[test]
1960    fn to_boxes_regular_single_block_matches_block_list_shape() {
1961        // A count=1 regular hyperslab is the same box a block-list form of
1962        // the same selection would produce.
1963        let sel = Selection::Hyperslab {
1964            rank: 1,
1965            form: Hyperslab::Regular(RegularHyperslab {
1966                start: vec![4],
1967                stride: vec![1],
1968                count: vec![1],
1969                block: vec![8],
1970            }),
1971        };
1972        assert_eq!(sel.to_boxes(&[20]).unwrap(), vec![(vec![4], vec![8])]);
1973    }
1974
1975    #[test]
1976    fn to_boxes_regular_zero_count_is_empty() {
1977        let sel = Selection::Hyperslab {
1978            rank: 1,
1979            form: Hyperslab::Regular(RegularHyperslab {
1980                start: vec![0],
1981                stride: vec![1],
1982                count: vec![0],
1983                block: vec![1],
1984            }),
1985        };
1986        assert_eq!(sel.to_boxes(&[10]).unwrap(), vec![]);
1987    }
1988
1989    #[test]
1990    fn to_boxes_regular_rejects_unlimited_count() {
1991        let sel = Selection::Hyperslab {
1992            rank: 1,
1993            form: Hyperslab::Regular(RegularHyperslab {
1994                start: vec![0],
1995                stride: vec![1],
1996                count: vec![UNLIMITED],
1997                block: vec![1],
1998            }),
1999        };
2000        let err = sel.to_boxes(&[10]).unwrap_err();
2001        assert!(matches!(err, FormatError::UnsupportedFeature(_)));
2002    }
2003
2004    #[test]
2005    fn to_boxes_regular_rejects_unlimited_block() {
2006        let sel = Selection::Hyperslab {
2007            rank: 1,
2008            form: Hyperslab::Regular(RegularHyperslab {
2009                start: vec![0],
2010                stride: vec![1],
2011                count: vec![1],
2012                block: vec![UNLIMITED],
2013            }),
2014        };
2015        let err = sel.to_boxes(&[10]).unwrap_err();
2016        assert!(matches!(err, FormatError::UnsupportedFeature(_)));
2017    }
2018
2019    /// A crafted `count` product over the box cap must fail cleanly instead
2020    /// of attempting a huge allocation.
2021    #[test]
2022    fn to_boxes_regular_rejects_huge_box_count() {
2023        let sel = Selection::Hyperslab {
2024            rank: 2,
2025            form: Hyperslab::Regular(RegularHyperslab {
2026                start: vec![0, 0],
2027                stride: vec![1, 1],
2028                count: vec![1 << 30, 1 << 30],
2029                block: vec![1, 1],
2030            }),
2031        };
2032        let err = sel.to_boxes(&[u64::MAX, u64::MAX]).unwrap_err();
2033        assert!(matches!(err, FormatError::UnsupportedFeature(_)));
2034    }
2035
2036    // ------------------------------------------------------------------ encode
2037
2038    /// Every fixture in `tests/fixtures/gen_selection_images.c`, matched
2039    /// byte-for-byte against `Selection::encode()` of the equivalent
2040    /// value — not just decode(encode(x)) == x, but the exact bytes
2041    /// libhdf5 1.14.6 itself wrote.
2042    ///
2043    /// A [`Hyperslab::Regular`] case's `decoded` value is its own
2044    /// Blocks-normalized equivalent, not the original `Regular` selection:
2045    /// regularity does not survive a real version-1 round trip either —
2046    /// libhdf5's own decode never reconstructs a REGULAR pattern from a
2047    /// block list, since the version-1 wire form has no flags byte to
2048    /// carry that information at all.
2049    #[test]
2050    fn selection_matches_libhdf5_image() {
2051        let cases: Vec<(&[u8], Selection, Selection)> = vec![
2052            (
2053                include_bytes!("../../tests/fixtures/all_v1.bin"),
2054                Selection::All,
2055                Selection::All,
2056            ),
2057            (
2058                include_bytes!("../../tests/fixtures/none_v1.bin"),
2059                Selection::None,
2060                Selection::None,
2061            ),
2062            (
2063                include_bytes!("../../tests/fixtures/hyperslab_single_block_v1.bin"),
2064                Selection::Hyperslab {
2065                    rank: 1,
2066                    form: Hyperslab::Blocks(vec![HyperslabBlock {
2067                        start: vec![4],
2068                        end: vec![11],
2069                    }]),
2070                },
2071                Selection::Hyperslab {
2072                    rank: 1,
2073                    form: Hyperslab::Blocks(vec![HyperslabBlock {
2074                        start: vec![4],
2075                        end: vec![11],
2076                    }]),
2077                },
2078            ),
2079            (
2080                include_bytes!("../../tests/fixtures/hyperslab_regular_3blocks_v1.bin"),
2081                // start=0, stride=5, count=3, block=2 — the exact Regular
2082                // form the C generator built via H5Sselect_hyperslab; the
2083                // captured image is nonetheless the version-1 block list
2084                // (see Selection::encode's doc comment), so encoding this
2085                // Regular value must reproduce those blocks byte-for-byte.
2086                Selection::Hyperslab {
2087                    rank: 1,
2088                    form: Hyperslab::Regular(RegularHyperslab {
2089                        start: vec![0],
2090                        stride: vec![5],
2091                        count: vec![3],
2092                        block: vec![2],
2093                    }),
2094                },
2095                Selection::Hyperslab {
2096                    rank: 1,
2097                    form: Hyperslab::Blocks(vec![
2098                        HyperslabBlock {
2099                            start: vec![0],
2100                            end: vec![1],
2101                        },
2102                        HyperslabBlock {
2103                            start: vec![5],
2104                            end: vec![6],
2105                        },
2106                        HyperslabBlock {
2107                            start: vec![10],
2108                            end: vec![11],
2109                        },
2110                    ]),
2111                },
2112            ),
2113            (
2114                include_bytes!("../../tests/fixtures/hyperslab_2d_regular_v1.bin"),
2115                Selection::Hyperslab {
2116                    rank: 2,
2117                    form: Hyperslab::Regular(RegularHyperslab {
2118                        start: vec![0, 0],
2119                        stride: vec![4, 4],
2120                        count: vec![2, 2],
2121                        block: vec![2, 2],
2122                    }),
2123                },
2124                Selection::Hyperslab {
2125                    rank: 2,
2126                    form: Hyperslab::Blocks(vec![
2127                        HyperslabBlock {
2128                            start: vec![0, 0],
2129                            end: vec![1, 1],
2130                        },
2131                        HyperslabBlock {
2132                            start: vec![0, 4],
2133                            end: vec![1, 5],
2134                        },
2135                        HyperslabBlock {
2136                            start: vec![4, 0],
2137                            end: vec![5, 1],
2138                        },
2139                        HyperslabBlock {
2140                            start: vec![4, 4],
2141                            end: vec![5, 5],
2142                        },
2143                    ]),
2144                },
2145            ),
2146            (
2147                include_bytes!("../../tests/fixtures/points4_v1.bin"),
2148                Selection::Points(PointSelection {
2149                    rank: 1,
2150                    points: vec![vec![1], vec![3], vec![7], vec![15]],
2151                }),
2152                Selection::Points(PointSelection {
2153                    rank: 1,
2154                    points: vec![vec![1], vec![3], vec![7], vec![15]],
2155                }),
2156            ),
2157        ];
2158
2159        for (blob, sel, want_decoded) in cases {
2160            let expected = strip_h5sencode_envelope(blob);
2161            let encoded = sel.encode().unwrap();
2162            assert_eq!(
2163                encoded, expected,
2164                "encode() mismatch for {sel:?}: got {encoded:02x?}, want {expected:02x?}"
2165            );
2166
2167            // And the image itself decodes back to the expected value, so
2168            // decode/encode agree on both ends of the wire, not just this
2169            // module's own encode output.
2170            let (decoded, consumed) = Selection::decode(expected).unwrap();
2171            assert_eq!(consumed, expected.len());
2172            assert_eq!(decoded, want_decoded);
2173        }
2174    }
2175
2176    /// decode(encode(x)) == x for [`Selection`] variants whose wire form
2177    /// is lossless (everything but [`Hyperslab::Regular`], which the
2178    /// version-1 wire form always normalizes into a block list — see
2179    /// `selection_matches_libhdf5_image`).
2180    #[test]
2181    fn encode_decode_round_trips() {
2182        let values = vec![
2183            Selection::None,
2184            Selection::All,
2185            Selection::Points(PointSelection {
2186                rank: 2,
2187                points: vec![vec![0, 0], vec![3, 5], vec![9, 1]],
2188            }),
2189            Selection::Hyperslab {
2190                rank: 2,
2191                form: Hyperslab::Blocks(vec![
2192                    HyperslabBlock {
2193                        start: vec![0, 0],
2194                        end: vec![1, 1],
2195                    },
2196                    HyperslabBlock {
2197                        start: vec![4, 4],
2198                        end: vec![5, 6],
2199                    },
2200                ]),
2201            },
2202        ];
2203        for sel in values {
2204            let encoded = sel.encode().unwrap();
2205            let (decoded, consumed) = Selection::decode(&encoded).unwrap();
2206            assert_eq!(consumed, encoded.len());
2207            assert_eq!(decoded, sel);
2208        }
2209    }
2210
2211    /// A [`Hyperslab::Regular`] value's round trip lands on its
2212    /// Blocks-normalized equivalent, not the original value — this is the
2213    /// same lossy-regularity behavior real libhdf5 has under the version-1
2214    /// wire form (no flags byte to carry a REGULAR marker at all).
2215    #[test]
2216    fn encode_decode_round_trips_regular_hyperslab_to_its_block_list() {
2217        let sel = Selection::Hyperslab {
2218            rank: 2,
2219            form: Hyperslab::Regular(RegularHyperslab {
2220                start: vec![1, 2],
2221                stride: vec![3, 3],
2222                count: vec![2, 3],
2223                block: vec![1, 2],
2224            }),
2225        };
2226        let encoded = sel.encode().unwrap();
2227        let (decoded, consumed) = Selection::decode(&encoded).unwrap();
2228        assert_eq!(consumed, encoded.len());
2229        let Selection::Hyperslab {
2230            form: Hyperslab::Blocks(blocks),
2231            ..
2232        } = decoded
2233        else {
2234            panic!("expected a decoded block list");
2235        };
2236        // 2 * 3 = 6 blocks, one per (start[d] + idx[d]*stride[d]) grid
2237        // point, each block[0]=1 x block[1]=2 wide.
2238        assert_eq!(blocks.len(), 6);
2239        assert_eq!(blocks[0].start, vec![1, 2]);
2240        assert_eq!(blocks[0].end, vec![1, 3]);
2241        assert_eq!(blocks[5].start, vec![4, 8]);
2242        assert_eq!(blocks[5].end, vec![4, 9]);
2243    }
2244
2245    #[test]
2246    fn encode_points_rejects_zero_rank() {
2247        let sel = Selection::Points(PointSelection {
2248            rank: 0,
2249            points: vec![],
2250        });
2251        let err = sel.encode().unwrap_err();
2252        assert!(matches!(err, FormatError::InvalidData(_)));
2253    }
2254
2255    #[test]
2256    fn encode_points_rejects_coordinate_length_mismatch() {
2257        let sel = Selection::Points(PointSelection {
2258            rank: 2,
2259            points: vec![vec![1, 2, 3]],
2260        });
2261        let err = sel.encode().unwrap_err();
2262        assert!(matches!(err, FormatError::InvalidData(_)));
2263    }
2264
2265    #[test]
2266    fn encode_points_rejects_coordinate_over_u32() {
2267        let sel = Selection::Points(PointSelection {
2268            rank: 1,
2269            points: vec![vec![1u64 << 40]],
2270        });
2271        let err = sel.encode().unwrap_err();
2272        assert!(matches!(err, FormatError::UnsupportedFeature(_)));
2273    }
2274
2275    #[test]
2276    fn encode_hyperslab_rejects_zero_rank() {
2277        let sel = Selection::Hyperslab {
2278            rank: 0,
2279            form: Hyperslab::Blocks(vec![]),
2280        };
2281        let err = sel.encode().unwrap_err();
2282        assert!(matches!(err, FormatError::InvalidData(_)));
2283    }
2284
2285    #[test]
2286    fn encode_hyperslab_rejects_block_length_mismatch() {
2287        let sel = Selection::Hyperslab {
2288            rank: 2,
2289            form: Hyperslab::Blocks(vec![HyperslabBlock {
2290                start: vec![0],
2291                end: vec![1],
2292            }]),
2293        };
2294        let err = sel.encode().unwrap_err();
2295        assert!(matches!(err, FormatError::InvalidData(_)));
2296    }
2297
2298    #[test]
2299    fn encode_hyperslab_rejects_regular_field_length_mismatch() {
2300        let sel = Selection::Hyperslab {
2301            rank: 2,
2302            form: Hyperslab::Regular(RegularHyperslab {
2303                start: vec![0],
2304                stride: vec![1],
2305                count: vec![1],
2306                block: vec![1],
2307            }),
2308        };
2309        let err = sel.encode().unwrap_err();
2310        assert!(matches!(err, FormatError::InvalidData(_)));
2311    }
2312
2313    #[test]
2314    fn encode_hyperslab_rejects_block_coordinate_over_u32() {
2315        let sel = Selection::Hyperslab {
2316            rank: 1,
2317            form: Hyperslab::Blocks(vec![HyperslabBlock {
2318                start: vec![0],
2319                end: vec![1u64 << 40],
2320            }]),
2321        };
2322        let err = sel.encode().unwrap_err();
2323        assert!(matches!(err, FormatError::UnsupportedFeature(_)));
2324    }
2325
2326    /// The libhdf5 image of an unlimited hyperslab, byte for byte.
2327    ///
2328    /// Captured from the virtual-dataset mapping list h5py 3.15.1 writes for
2329    /// `layout[:h5py.h5s.UNLIMITED, :] = vsrc[:h5py.h5s.UNLIMITED, :]` over a
2330    /// rank-2 `(1, 2)` layout — the two selections are identical, so this is
2331    /// the image of both. An unlimited dimension forces version 2
2332    /// (`H5S__hyper_get_version_enc_size`), whose REGULAR form spells the
2333    /// unlimited count as an all-ones 8-byte field.
2334    const LIBHDF5_UNLIMITED_HYPERSLAB: &[u8] = &[
2335        0x02, 0x00, 0x00, 0x00, // H5S_SEL_HYPERSLABS
2336        0x02, 0x00, 0x00, 0x00, // version 2
2337        0x01, // flags: H5S_HYPER_REGULAR
2338        0x44, 0x00, 0x00, 0x00, // length: 4 + 32 * rank
2339        0x02, 0x00, 0x00, 0x00, // rank
2340        // dim 0: start 0, stride 1, count H5S_UNLIMITED, block 1
2341        0, 0, 0, 0, 0, 0, 0, 0, //
2342        1, 0, 0, 0, 0, 0, 0, 0, //
2343        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, //
2344        1, 0, 0, 0, 0, 0, 0, 0, //
2345        // dim 1: start 0, stride 1, count 1, block 2
2346        0, 0, 0, 0, 0, 0, 0, 0, //
2347        1, 0, 0, 0, 0, 0, 0, 0, //
2348        1, 0, 0, 0, 0, 0, 0, 0, //
2349        2, 0, 0, 0, 0, 0, 0, 0, //
2350    ];
2351
2352    fn unlimited_rows() -> Selection {
2353        Selection::Hyperslab {
2354            rank: 2,
2355            form: Hyperslab::Regular(RegularHyperslab {
2356                start: vec![0, 0],
2357                stride: vec![1, 1],
2358                count: vec![UNLIMITED, 1],
2359                block: vec![1, 2],
2360            }),
2361        }
2362    }
2363
2364    /// An unlimited count encodes to exactly what libhdf5 writes: the
2365    /// version-2 REGULAR form, not the version-1 block list every bounded
2366    /// hyperslab takes.
2367    #[test]
2368    fn unlimited_hyperslab_encodes_as_the_libhdf5_version_2_image() {
2369        assert_eq!(
2370            unlimited_rows().encode().unwrap(),
2371            LIBHDF5_UNLIMITED_HYPERSLAB
2372        );
2373    }
2374
2375    /// And the decoder reads that image back as the same selection, so a
2376    /// mapping list survives a write/read round trip unchanged.
2377    #[test]
2378    fn unlimited_hyperslab_decodes_from_the_libhdf5_image() {
2379        let (sel, used) = Selection::decode(LIBHDF5_UNLIMITED_HYPERSLAB).unwrap();
2380        assert_eq!(used, LIBHDF5_UNLIMITED_HYPERSLAB.len());
2381        assert_eq!(sel, unlimited_rows());
2382        assert_eq!(sel.unlim_dim(), Some(0));
2383    }
2384
2385    /// An unlimited selection has no bounds — `H5Sget_select_bounds` fails on
2386    /// one, which is why the oracle's canon renders it as `?`.
2387    #[test]
2388    fn an_unlimited_selection_has_no_bounds() {
2389        assert_eq!(unlimited_rows().bounds(), None);
2390    }
2391
2392    /// `H5S_hyper_clip_unlim`: the unlimited dimension is cut to the extent,
2393    /// and the result covers exactly the elements inside it.
2394    #[test]
2395    fn clip_unlimited_cuts_the_unlimited_dimension_to_the_extent() {
2396        let clipped = unlimited_rows().clip_unlimited(3).unwrap();
2397        assert_eq!(
2398            // `H5S__hyper_get_clip_diminfo` collapses a contiguous run
2399            // (block == stride) into one block of the whole extent, so the
2400            // three unit rows come back as one 3-row box.
2401            clipped.to_boxes(&[3, 2]).unwrap(),
2402            vec![(vec![0, 0], vec![3, 2])]
2403        );
2404        // Nothing available: the mapping selects nothing at all, rather than
2405        // an empty block list.
2406        assert_eq!(unlimited_rows().clip_unlimited(0).unwrap(), Selection::None);
2407        // A bounded selection is its own clip.
2408        assert_eq!(Selection::All.clip_unlimited(7).unwrap(), Selection::All);
2409    }
2410
2411    /// A stride wider than the block leaves a gap, and the extent can cut
2412    /// through the middle of a block — `H5S__hyper_get_clip_diminfo`'s
2413    /// unlimited-count arm, where the last block comes back short.
2414    #[test]
2415    fn clip_unlimited_truncates_the_block_the_extent_cuts_through() {
2416        let sel = Selection::Hyperslab {
2417            rank: 1,
2418            form: Hyperslab::Regular(RegularHyperslab {
2419                start: vec![1],
2420                stride: vec![4],
2421                count: vec![UNLIMITED],
2422                block: vec![3],
2423            }),
2424        };
2425        // Blocks at [1,4) and [5,8); an extent of 7 cuts the second short.
2426        assert_eq!(
2427            sel.clip_unlimited(7).unwrap().to_boxes(&[7]).unwrap(),
2428            vec![(vec![1], vec![3]), (vec![5], vec![2])]
2429        );
2430    }
2431
2432    /// `H5S_hyper_get_clip_extent_match`, both halves: how many slices a
2433    /// source extent supplies, and the virtual extent that covers exactly
2434    /// that many.
2435    #[test]
2436    fn clip_extent_matches_the_slices_the_source_supplies() {
2437        let Selection::Hyperslab {
2438            form: Hyperslab::Regular(rows),
2439            ..
2440        } = unlimited_rows()
2441        else {
2442            unreachable!()
2443        };
2444        // Contiguous unit blocks: slices and extent are the source extent.
2445        for extent in [0u64, 1, 6, 10] {
2446            assert_eq!(rows.num_slices(extent), extent);
2447            assert_eq!(rows.clip_extent(extent, false), extent);
2448        }
2449
2450        // Strided blocks: 3 elements every 4, so an extent of 10 supplies
2451        // 3 + 3 + 2 = 8 slices, and covering 8 slices needs an extent of 10.
2452        let strided = RegularHyperslab {
2453            start: vec![0],
2454            stride: vec![4],
2455            count: vec![UNLIMITED],
2456            block: vec![3],
2457        };
2458        assert_eq!(strided.num_slices(10), 8);
2459        assert_eq!(strided.clip_extent(8, false), 10);
2460        // Two whole blocks: `incl_trail` is the difference between ending at
2461        // the last block's end and ending before the first missing block.
2462        assert_eq!(strided.clip_extent(6, false), 7);
2463        assert_eq!(strided.clip_extent(6, true), 8);
2464        assert_eq!(strided.clip_extent(0, false), 0);
2465    }
2466
2467    /// `H5S_get_select_num_elem_non_unlim` and `H5S_hyper_get_unlim_block`:
2468    /// the per-slice element count two unlimited selections must agree on,
2469    /// and the single block a printf mapping's `index`-th source fills.
2470    #[test]
2471    fn unlimited_slice_shape_and_block_extraction() {
2472        let Selection::Hyperslab {
2473            form: Hyperslab::Regular(rows),
2474            ..
2475        } = unlimited_rows()
2476        else {
2477            unreachable!()
2478        };
2479        assert_eq!(rows.num_elem_non_unlim(), Some(2));
2480        let third = rows.unlim_block(3);
2481        assert_eq!(third.start, vec![3, 0]);
2482        assert_eq!(third.count, vec![1, 1]);
2483        assert_eq!(third.block, vec![1, 2]);
2484        assert_eq!(third.unlim_dim(), None);
2485    }
2486}