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