Skip to main content

hdf5_pure/
vl_data.rs

1//! Variable-length data reading (VL strings & VL sequences).
2//!
3//! VL data elements in HDF5 store their values in the global heap.
4//! The raw data for each element contains a global heap ID:
5//! `sequence_length(4 LE) + collection_address(offset_size LE) + object_index(4 LE)`.
6
7#[cfg(not(feature = "std"))]
8use alloc::{format, string::String, vec::Vec};
9
10use crate::convert::{TryToUsize, is_undefined_addr};
11use crate::datatype::{CharacterSet, Datatype};
12use crate::error::FormatError;
13use crate::global_heap::GlobalHeapIndex;
14#[cfg(test)]
15use crate::source::BytesSource;
16use crate::source::Source;
17
18/// Allocation limits for reading variable-length strings.
19///
20/// Limits are checked before any string payload is materialized. The payload
21/// byte limit covers the bytes referenced by the VL elements; it excludes the
22/// `Vec<String>` and `String` allocation metadata.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub struct VlenStringReadOptions {
25    max_elements: Option<usize>,
26    max_payload_bytes: Option<usize>,
27}
28
29impl VlenStringReadOptions {
30    /// Create options with no limits.
31    pub const fn new() -> Self {
32        Self {
33            max_elements: None,
34            max_payload_bytes: None,
35        }
36    }
37
38    /// Set the maximum number of VL elements that may be read.
39    pub const fn with_max_elements(mut self, max_elements: usize) -> Self {
40        self.max_elements = Some(max_elements);
41        self
42    }
43
44    /// Set the maximum total string payload size in bytes.
45    pub const fn with_max_payload_bytes(mut self, max_payload_bytes: usize) -> Self {
46        self.max_payload_bytes = Some(max_payload_bytes);
47        self
48    }
49
50    /// Return the configured element limit.
51    pub const fn max_elements(&self) -> Option<usize> {
52        self.max_elements
53    }
54
55    /// Return the configured payload-byte limit.
56    pub const fn max_payload_bytes(&self) -> Option<usize> {
57        self.max_payload_bytes
58    }
59}
60
61/// A parsed variable-length element reference (global heap ID).
62#[derive(Debug, Clone)]
63pub struct VlElement {
64    /// Length of the VL data.
65    pub length: u32,
66    /// Address of the global heap collection containing the data.
67    pub collection_address: u64,
68    /// Index of the object within the collection.
69    pub object_index: u32,
70}
71
72fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
73    match offset.checked_add(needed) {
74        Some(end) if end <= data.len() => Ok(()),
75        _ => Err(FormatError::UnexpectedEof {
76            expected: offset.saturating_add(needed),
77            available: data.len(),
78        }),
79    }
80}
81
82fn read_offset(data: &[u8], pos: usize, offset_size: u8) -> Result<u64, FormatError> {
83    let s = offset_size as usize;
84    ensure_len(data, pos, s)?;
85    let slice = &data[pos..pos + s];
86    Ok(match offset_size {
87        2 => u16::from_le_bytes([slice[0], slice[1]]) as u64,
88        4 => u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as u64,
89        8 => u64::from_le_bytes([
90            slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
91        ]),
92        _ => return Err(FormatError::InvalidOffsetSize(offset_size)),
93    })
94}
95
96/// Parse VL global heap references from raw attribute/dataset data.
97pub fn parse_vl_references(
98    raw_data: &[u8],
99    num_elements: u64,
100    offset_size: u8,
101) -> Result<Vec<VlElement>, FormatError> {
102    let elem_size = 4 + offset_size as u64 + 4; // length + address + index
103    let total = num_elements
104        .checked_mul(elem_size)
105        .ok_or(FormatError::OffsetOverflow {
106            offset: num_elements,
107            length: elem_size,
108        })?
109        .to_usize()?;
110    if raw_data.len() < total {
111        return Err(FormatError::UnexpectedEof {
112            expected: total,
113            available: raw_data.len(),
114        });
115    }
116
117    let mut elements = Vec::with_capacity(num_elements.to_usize()?);
118    let mut pos = 0;
119
120    for _ in 0..num_elements {
121        let length = u32::from_le_bytes([
122            raw_data[pos],
123            raw_data[pos + 1],
124            raw_data[pos + 2],
125            raw_data[pos + 3],
126        ]);
127        pos += 4;
128
129        let collection_address = read_offset(raw_data, pos, offset_size)?;
130        pos += offset_size as usize;
131
132        let object_index = u32::from_le_bytes([
133            raw_data[pos],
134            raw_data[pos + 1],
135            raw_data[pos + 2],
136            raw_data[pos + 3],
137        ]);
138        pos += 4;
139
140        elements.push(VlElement {
141            length,
142            collection_address,
143            object_index,
144        });
145    }
146
147    Ok(elements)
148}
149
150/// Whether a datatype is one of the string-shaped VL encodings understood by
151/// this module.
152pub(crate) fn is_vlen_string_datatype(datatype: &Datatype) -> bool {
153    match datatype {
154        Datatype::VariableLength {
155            is_string: true, ..
156        } => true,
157        Datatype::VariableLength {
158            is_string: false,
159            base_type,
160            ..
161        } => matches!(
162            base_type.as_ref(),
163            Datatype::String {
164                size: 1,
165                charset: CharacterSet::Ascii,
166                ..
167            }
168        ),
169        _ => false,
170    }
171}
172
173/// A variable-length reference reached *inside* a larger element rather than
174/// being the element itself: a member of a compound, or an entry of an array of
175/// them. Repack needs both coordinates to re-stage the payload and rewrite the
176/// reference in place.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub(crate) struct EmbeddedVlSlot {
179    /// Byte offset of the 16-byte reference within one element of the dataset.
180    pub byte_offset: usize,
181    /// Byte width of the variable-length base type. The reference's stored
182    /// `length` counts base-type elements, so the heap object holds
183    /// `length * element_size` bytes.
184    pub element_size: usize,
185}
186
187/// A dataset with embedded variable-length references, read for rewriting: the
188/// element bytes as they stand, plus each reference's position within them and
189/// the heap payload it names.
190pub(crate) struct EmbeddedVlData {
191    /// The dataset's element bytes, references still carrying source addresses.
192    pub raw: Vec<u8>,
193    /// Byte offset within `raw` of each embedded reference.
194    pub offsets: Vec<usize>,
195    /// The heap payload each reference names, paired in order with `offsets`.
196    pub objects: Vec<VlByteObject>,
197}
198
199/// Every variable-length reference `datatype` reaches through a compound member
200/// or array entry, in declaration order.
201///
202/// A datatype that *is* variable-length yields the single slot at offset 0, so
203/// callers that handle the top-level case separately should test for that first.
204/// Returns `None` if the datatype's declared size cannot hold the slots found,
205/// which means either a malformed datatype or dimensions that overflowed — in
206/// both cases the element bytes cannot be walked safely.
207pub(crate) fn embedded_vlen_slots(datatype: &Datatype) -> Option<Vec<EmbeddedVlSlot>> {
208    // A reference is 16 bytes, so an element can hold no more than this many.
209    // Bounding the walk keeps a datatype declaring absurd array dimensions from
210    // driving an unbounded allocation here.
211    let element_size = datatype.type_size() as usize;
212    let capacity = element_size / VL_REF_BYTES;
213    let mut slots = Vec::new();
214    if !collect_vlen_slots(datatype, 0, capacity, &mut slots) {
215        return None;
216    }
217    // `checked_add`: an offset near the top of the address space would otherwise
218    // wrap here and read as "fits".
219    if slots.len() > capacity
220        || slots.iter().any(|s| {
221            s.byte_offset
222                .checked_add(VL_REF_BYTES)
223                .is_none_or(|end| end > element_size)
224        })
225    {
226        return None;
227    }
228    Some(slots)
229}
230
231/// On-disk width of a variable-length reference with 8-byte offsets, which is
232/// the only offset size the write path emits and the only one repack re-stages.
233const VL_REF_BYTES: usize = 16;
234
235/// Walk `datatype`, appending a slot for each variable-length reference it
236/// reaches. Returns `false` if it cannot be walked on this target: an offset the
237/// file declares that does not fit `usize`, or one that overflows it, names a
238/// position this platform cannot address, and silently truncating it would place
239/// a slot somewhere plausible but wrong.
240fn collect_vlen_slots(
241    datatype: &Datatype,
242    base: usize,
243    capacity: usize,
244    out: &mut Vec<EmbeddedVlSlot>,
245) -> bool {
246    // Stop one past the bound so the caller can still tell "too many" from "fits".
247    if out.len() > capacity {
248        return true;
249    }
250    match datatype {
251        Datatype::VariableLength { base_type, .. } => {
252            // A VL string's reference counts bytes (its base type is one byte
253            // wide); a sequence's counts base-type elements.
254            let element_size = if is_vlen_string_datatype(datatype) {
255                1
256            } else {
257                (base_type.type_size() as usize).max(1)
258            };
259            out.push(EmbeddedVlSlot {
260                byte_offset: base,
261                element_size,
262            });
263            true
264        }
265        Datatype::Compound { members, .. } => {
266            for m in members {
267                let Some(at) = usize::try_from(m.byte_offset)
268                    .ok()
269                    .and_then(|off| base.checked_add(off))
270                else {
271                    return false;
272                };
273                if !collect_vlen_slots(&m.datatype, at, capacity, out) {
274                    return false;
275                }
276            }
277            true
278        }
279        Datatype::Array {
280            base_type,
281            dimensions,
282        } => {
283            // If the entry type reaches no reference then no repetition of it
284            // does either. Checking once keeps a datatype declaring huge
285            // dimensions from spinning through entries that can never contribute,
286            // and makes every iteration below push at least one slot — so the
287            // `capacity` bound terminates the loop.
288            // Walk the entry type *once*, at offset 0, then translate that result
289            // to each entry's base. Re-walking per entry would recompute the same
290            // sub-tree `entries` times at every level of nesting, which is
291            // exponential in nesting depth: a ~300-byte datatype of nested arrays
292            // is enough to burn hours of CPU on work whose output is bounded by
293            // `capacity`.
294            let mut probe = Vec::new();
295            if !collect_vlen_slots(base_type, 0, capacity, &mut probe) {
296                return false;
297            }
298            // No reference in one entry means none in any repetition of it.
299            if probe.is_empty() {
300                return true;
301            }
302            let count = dimensions
303                .iter()
304                .copied()
305                .fold(1u64, |a, b| a.saturating_mul(u64::from(b)));
306            // Every entry contributes at least one slot, so more entries than the
307            // element has room for cannot fit however the walk goes. Reject up
308            // front rather than materializing that many slots for the caller to
309            // discard: for a datatype whose declared size saturates, `capacity`
310            // runs to hundreds of millions, which is merely wasteful on a 64-bit
311            // target and an outright allocation failure on a 32-bit one.
312            if count > capacity as u64 {
313                return false;
314            }
315            let entries = usize::try_from(count).unwrap_or(usize::MAX);
316            let stride = base_type.type_size() as usize;
317            for i in 0..entries {
318                let Some(at) = i.checked_mul(stride).and_then(|off| base.checked_add(off)) else {
319                    return false;
320                };
321                for slot in &probe {
322                    let Some(byte_offset) = at.checked_add(slot.byte_offset) else {
323                        return false;
324                    };
325                    out.push(EmbeddedVlSlot {
326                        byte_offset,
327                        element_size: slot.element_size,
328                    });
329                    // Stop one past the bound so the caller can still tell
330                    // "too many" from "fits".
331                    if out.len() > capacity {
332                        return true;
333                    }
334                }
335            }
336            true
337        }
338        // An enumeration's base is an integer, and no other class can reach a
339        // variable-length reference.
340        _ => true,
341    }
342}
343
344fn check_element_limit(
345    num_elements: u64,
346    options: VlenStringReadOptions,
347) -> Result<(), FormatError> {
348    if let Some(limit) = options.max_elements
349        && num_elements > limit as u64
350    {
351        return Err(FormatError::VariableLengthElementLimitExceeded {
352            limit,
353            actual: num_elements,
354        });
355    }
356    Ok(())
357}
358
359fn payload_size(refs: &[VlElement], options: VlenStringReadOptions) -> Result<u64, FormatError> {
360    let mut required = 0u64;
361    for element in refs {
362        required =
363            required
364                .checked_add(u64::from(element.length))
365                .ok_or(FormatError::OffsetOverflow {
366                    offset: required,
367                    length: u64::from(element.length),
368                })?;
369    }
370    if let Some(limit) = options.max_payload_bytes
371        && required > limit as u64
372    {
373        return Err(FormatError::VariableLengthByteLimitExceeded { limit, required });
374    }
375    Ok(required)
376}
377
378/// Return the total payload bytes named by a set of VL references.
379pub fn vlen_string_payload_size(
380    raw_data: &[u8],
381    num_elements: u64,
382    offset_size: u8,
383) -> Result<u64, FormatError> {
384    check_element_limit(num_elements, VlenStringReadOptions::default())?;
385    let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
386    payload_size(&refs, VlenStringReadOptions::default())
387}
388
389/// Resolve VL strings from a random-access file source and pass them to a
390/// visitor one at a time.
391pub fn visit_vl_strings_from_source<S, F>(
392    source: &S,
393    raw_data: &[u8],
394    num_elements: u64,
395    offset_size: u8,
396    length_size: u8,
397    base_address: u64,
398    options: VlenStringReadOptions,
399    mut visitor: F,
400) -> Result<(), FormatError>
401where
402    S: Source + ?Sized,
403    F: FnMut(&str),
404{
405    check_element_limit(num_elements, options)?;
406    let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
407    payload_size(&refs, options)?;
408
409    // This call's object indices per (base-adjusted) collection address, so
410    // each collection's directory walk retains only the entries the call
411    // resolves — a row window of a large dataset would otherwise be charged
412    // the full directory of every touched collection (a writer may pack every
413    // string into one collection). Grouping collects only references the
414    // resolve loop below will look up; invalid ones surface their errors
415    // there, in element order.
416    let mut wanted: Vec<(u64, Vec<u16>)> = Vec::new();
417    for element in &refs {
418        if is_undefined_addr(element.collection_address, offset_size)
419            || (element.length == 0 && element.collection_address == 0)
420        {
421            continue;
422        }
423        let Some(address) = element.collection_address.checked_add(base_address) else {
424            continue;
425        };
426        let Ok(index) = u16::try_from(element.object_index) else {
427            continue;
428        };
429        match wanted.binary_search_by_key(&address, |&(a, _)| a) {
430            Ok(pos) => wanted[pos].1.push(index),
431            Err(pos) => wanted.insert(pos, (address, Vec::from([index]))),
432        }
433    }
434    for (_, indices) in &mut wanted {
435        indices.sort_unstable();
436    }
437
438    let mut collections: Vec<(u64, GlobalHeapIndex)> = Vec::new();
439    for element in &refs {
440        if element.length == 0
441            && (is_undefined_addr(element.collection_address, offset_size)
442                || element.collection_address == 0)
443        {
444            visitor("");
445            continue;
446        }
447        if is_undefined_addr(element.collection_address, offset_size) {
448            return Err(FormatError::VlDataError(
449                "non-empty VL element has an undefined heap address".into(),
450            ));
451        }
452
453        let collection_address = element.collection_address.checked_add(base_address).ok_or(
454            FormatError::OffsetOverflow {
455                offset: element.collection_address,
456                length: base_address,
457            },
458        )?;
459        let collection_pos = match collections
460            .iter()
461            .position(|(address, _)| *address == collection_address)
462        {
463            Some(pos) => pos,
464            None => {
465                let keep = wanted
466                    .binary_search_by_key(&collection_address, |&(a, _)| a)
467                    .map(|pos| wanted[pos].1.as_slice())
468                    .unwrap_or(&[]);
469                let collection = GlobalHeapIndex::parse_filtered(
470                    source,
471                    collection_address,
472                    length_size,
473                    |i| keep.binary_search(&i).is_ok(),
474                )?;
475                collections.push((collection_address, collection));
476                collections.len() - 1
477            }
478        };
479
480        let index = u16::try_from(element.object_index).map_err(|_| {
481            FormatError::VlDataError(format!(
482                "global heap object index {} does not fit u16",
483                element.object_index
484            ))
485        })?;
486        let object = collections[collection_pos].1.get_object(index).ok_or(
487            FormatError::GlobalHeapObjectNotFound {
488                collection_address,
489                index,
490            },
491        )?;
492        if u64::from(element.length) > object.size {
493            return Err(FormatError::VlDataError(format!(
494                "VL element length {} exceeds global heap object size {}",
495                element.length, object.size
496            )));
497        }
498
499        let bytes = source.read_exact_at(object.data_address, element.length as usize)?;
500        let string = String::from_utf8_lossy(&bytes);
501        visitor(&string);
502    }
503
504    Ok(())
505}
506
507/// Resolve VL strings from a random-access file source.
508pub fn read_vl_strings_from_source<S: Source + ?Sized>(
509    source: &S,
510    raw_data: &[u8],
511    num_elements: u64,
512    offset_size: u8,
513    length_size: u8,
514    base_address: u64,
515    options: VlenStringReadOptions,
516) -> Result<Vec<String>, FormatError> {
517    let mut strings = Vec::new();
518    visit_vl_strings_from_source(
519        source,
520        raw_data,
521        num_elements,
522        offset_size,
523        length_size,
524        base_address,
525        options,
526        |string| strings.push(String::from(string)),
527    )?;
528    Ok(strings)
529}
530
531/// One element of a variable-length string dataset/attribute, read as exact
532/// heap bytes rather than a lossily-decoded `String`.
533///
534/// `None` is a *null* reference (length 0 with an undefined or zero heap
535/// address), which the HDF5 model distinguishes from an empty string. `Some`
536/// is a real heap object, carrying its exact bytes (possibly empty, possibly
537/// containing embedded NULs or non-UTF-8 sequences). Preserving this
538/// distinction lets a faithful rewrite reproduce the source byte-for-byte.
539#[derive(Debug, Clone, PartialEq, Eq)]
540pub(crate) enum VlByteObject {
541    /// A null VL reference (no heap object).
542    Null,
543    /// A heap object holding these exact bytes.
544    Bytes(Vec<u8>),
545}
546
547/// Resolve a VL element's exact heap bytes from a random-access source,
548/// preserving the null-vs-empty distinction and never lossily decoding.
549///
550/// This mirrors [`visit_vl_strings_from_source`] but yields raw bytes (and a
551/// null marker) instead of a `&str`, so a faithful rewrite can reproduce
552/// embedded-NUL and non-UTF-8 payloads exactly.
553///
554/// `element_size` is the byte width of one base-type element of the sequence.
555/// For VL strings the base type is a single byte, so `element_size == 1` and the
556/// reference's stored `length` (an element count) equals the byte count. For a
557/// non-string VL sequence (e.g. `H5T_VLEN { H5T_NATIVE_DOUBLE }`) the stored
558/// `length` counts base-type elements, so the heap object holds
559/// `length * element_size` bytes — exactly what is read here.
560pub(crate) fn read_vl_byte_objects_from_source<S: Source + ?Sized>(
561    source: &S,
562    raw_data: &[u8],
563    num_elements: u64,
564    offset_size: u8,
565    length_size: u8,
566    base_address: u64,
567    element_size: usize,
568    options: VlenStringReadOptions,
569) -> Result<Vec<VlByteObject>, FormatError> {
570    check_element_limit(num_elements, options)?;
571    let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
572    payload_size(&refs, options)?;
573
574    let mut objects = Vec::with_capacity(refs.len());
575    let mut collections: Vec<(u64, GlobalHeapIndex)> = Vec::new();
576    for element in &refs {
577        if element.length == 0
578            && (is_undefined_addr(element.collection_address, offset_size)
579                || element.collection_address == 0)
580        {
581            objects.push(VlByteObject::Null);
582            continue;
583        }
584        if is_undefined_addr(element.collection_address, offset_size) {
585            return Err(FormatError::VlDataError(
586                "non-empty VL element has an undefined heap address".into(),
587            ));
588        }
589
590        let collection_address = element.collection_address.checked_add(base_address).ok_or(
591            FormatError::OffsetOverflow {
592                offset: element.collection_address,
593                length: base_address,
594            },
595        )?;
596        let collection_pos = match collections
597            .iter()
598            .position(|(address, _)| *address == collection_address)
599        {
600            Some(pos) => pos,
601            None => {
602                let collection = GlobalHeapIndex::parse(source, collection_address, length_size)?;
603                collections.push((collection_address, collection));
604                collections.len() - 1
605            }
606        };
607
608        let index = u16::try_from(element.object_index).map_err(|_| {
609            FormatError::VlDataError(format!(
610                "global heap object index {} does not fit u16",
611                element.object_index
612            ))
613        })?;
614        let object = collections[collection_pos].1.get_object(index).ok_or(
615            FormatError::GlobalHeapObjectNotFound {
616                collection_address,
617                index,
618            },
619        )?;
620        // The heap object holds `length` base-type elements of `element_size`
621        // bytes each. Compute the byte count with checked arithmetic so a hostile
622        // `length` cannot overflow, and bound it by the heap object's own size.
623        let byte_len = (element.length as u64)
624            .checked_mul(element_size as u64)
625            .ok_or(FormatError::OffsetOverflow {
626                offset: u64::from(element.length),
627                length: element_size as u64,
628            })?;
629        if byte_len > object.size {
630            return Err(FormatError::VlDataError(format!(
631                "VL element length {} ({} bytes) exceeds global heap object size {}",
632                element.length, byte_len, object.size
633            )));
634        }
635
636        let bytes = source.read_exact_at(object.data_address, byte_len.to_usize()?)?;
637        objects.push(VlByteObject::Bytes(bytes));
638    }
639
640    Ok(objects)
641}
642
643/// Resolve VL strings from an in-memory buffer by looking up each element in the
644/// global heap. A thin convenience wrapper over
645/// [`read_vl_strings_from_source`] used by the unit tests; production callers go
646/// straight to the source-based reader so a streaming backend works unchanged.
647#[cfg(test)]
648pub fn read_vl_strings(
649    file_data: &[u8],
650    raw_data: &[u8],
651    num_elements: u64,
652    offset_size: u8,
653    length_size: u8,
654) -> Result<Vec<String>, FormatError> {
655    read_vl_strings_from_source(
656        &BytesSource::new(file_data),
657        raw_data,
658        num_elements,
659        offset_size,
660        length_size,
661        0,
662        VlenStringReadOptions::default(),
663    )
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    /// Build a global heap collection at given offset in a file buffer.
671    fn build_gcol_at(
672        file_data: &mut Vec<u8>,
673        offset: usize,
674        objects: &[(u16, &[u8])], // (index, data)
675    ) {
676        let length_size = 8usize;
677
678        // Ensure file_data is large enough
679        let header_size = 8 + length_size;
680        let mut obj_total = 0usize;
681        for (_, data) in objects {
682            let padded = (data.len() + 7) & !7;
683            obj_total += 8 + length_size + padded;
684        }
685        obj_total += 2; // free space marker
686        let collection_size = header_size + obj_total;
687        let needed = offset + collection_size;
688        if file_data.len() < needed {
689            file_data.resize(needed, 0);
690        }
691
692        let mut pos = offset;
693        // Signature
694        file_data[pos..pos + 4].copy_from_slice(b"GCOL");
695        file_data[pos + 4] = 1; // version
696        // reserved(3) already 0
697        pos += 8;
698        file_data[pos..pos + 8].copy_from_slice(&(collection_size as u64).to_le_bytes());
699        pos += 8;
700
701        for (index, data) in objects {
702            file_data[pos..pos + 2].copy_from_slice(&index.to_le_bytes());
703            file_data[pos + 2..pos + 4].copy_from_slice(&1u16.to_le_bytes()); // ref_count
704            // reserved(4) already 0
705            pos += 8;
706            file_data[pos..pos + 8].copy_from_slice(&(data.len() as u64).to_le_bytes());
707            pos += 8;
708            file_data[pos..pos + data.len()].copy_from_slice(data);
709            let padded = (data.len() + 7) & !7;
710            pos += padded;
711        }
712        // free space marker
713        file_data[pos..pos + 2].copy_from_slice(&0u16.to_le_bytes());
714    }
715
716    /// Build VL reference raw data for given strings at a collection address.
717    fn build_vl_refs(
718        strings: &[&str],
719        collection_address: u64,
720        start_index: u16,
721        offset_size: u8,
722    ) -> Vec<u8> {
723        let mut raw = Vec::new();
724        for (i, s) in strings.iter().enumerate() {
725            raw.extend_from_slice(&(s.len() as u32).to_le_bytes());
726            match offset_size {
727                4 => raw.extend_from_slice(&(collection_address as u32).to_le_bytes()),
728                8 => raw.extend_from_slice(&collection_address.to_le_bytes()),
729                _ => panic!("unsupported"),
730            }
731            raw.extend_from_slice(&(start_index as u32 + i as u32).to_le_bytes());
732        }
733        raw
734    }
735
736    #[test]
737    fn parse_vl_references_two_elements() {
738        let raw = build_vl_refs(&["hello", "world"], 0x1000, 1, 8);
739        let refs = parse_vl_references(&raw, 2, 8).unwrap();
740        assert_eq!(refs.len(), 2);
741        assert_eq!(refs[0].length, 5);
742        assert_eq!(refs[0].collection_address, 0x1000);
743        assert_eq!(refs[0].object_index, 1);
744        assert_eq!(refs[1].length, 5);
745        assert_eq!(refs[1].object_index, 2);
746    }
747
748    #[test]
749    fn read_vl_strings_from_heap() {
750        let gcol_offset = 256usize;
751        let mut file_data = vec![0u8; 512];
752        build_gcol_at(&mut file_data, gcol_offset, &[(1, b"Alice"), (2, b"Bob")]);
753
754        let raw = build_vl_refs(&["Alice", "Bob"], gcol_offset as u64, 1, 8);
755        let strings = read_vl_strings(&file_data, &raw, 2, 8, 8).unwrap();
756        assert_eq!(strings, vec!["Alice", "Bob"]);
757    }
758
759    #[cfg(feature = "std")]
760    #[test]
761    fn read_vl_strings_from_seekable_source() {
762        use std::io::Cursor;
763
764        use crate::source::ReadSeekSource;
765
766        let gcol_offset = 256usize;
767        let mut file_data = vec![0u8; 512];
768        build_gcol_at(&mut file_data, gcol_offset, &[(1, b"Alice"), (2, b"Bob")]);
769        let raw = build_vl_refs(&["Alice", "Bob"], gcol_offset as u64, 1, 8);
770        let source = ReadSeekSource::new(Cursor::new(file_data)).unwrap();
771
772        let strings = read_vl_strings_from_source(
773            &source,
774            &raw,
775            2,
776            8,
777            8,
778            0,
779            VlenStringReadOptions::default(),
780        )
781        .unwrap();
782        assert_eq!(strings, vec!["Alice", "Bob"]);
783    }
784
785    #[test]
786    fn null_vl_element_empty_string() {
787        // length=0, address=undefined
788        let mut raw = Vec::new();
789        raw.extend_from_slice(&0u32.to_le_bytes()); // length=0
790        raw.extend_from_slice(&u64::MAX.to_le_bytes()); // undefined address
791        raw.extend_from_slice(&0u32.to_le_bytes()); // index
792
793        let file_data = vec![0u8; 16];
794        let strings = read_vl_strings(&file_data, &raw, 1, 8, 8).unwrap();
795        assert_eq!(strings, vec![""]);
796    }
797
798    #[test]
799    fn null_vl_element_zero_address() {
800        let mut raw = Vec::new();
801        raw.extend_from_slice(&0u32.to_le_bytes());
802        raw.extend_from_slice(&0u64.to_le_bytes());
803        raw.extend_from_slice(&0u32.to_le_bytes());
804
805        let file_data = vec![0u8; 16];
806        let strings = read_vl_strings(&file_data, &raw, 1, 8, 8).unwrap();
807        assert_eq!(strings, vec![""]);
808    }
809
810    #[test]
811    fn parse_vl_references_truncated_error() {
812        let raw = vec![0u8; 10]; // too short for 1 element with offset_size=8
813        let err = parse_vl_references(&raw, 1, 8).unwrap_err();
814        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
815    }
816}
817
818#[cfg(test)]
819mod embedded_slot_tests {
820    use super::*;
821    use crate::datatype::{CompoundMember, StringPadding};
822
823    fn vlen_string() -> Datatype {
824        Datatype::VariableLength {
825            is_string: true,
826            base_type: Box::new(Datatype::String {
827                size: 1,
828                charset: CharacterSet::Utf8,
829                padding: StringPadding::NullTerminate,
830            }),
831            padding: Some(StringPadding::NullTerminate),
832            charset: Some(CharacterSet::Utf8),
833        }
834    }
835
836    fn vlen_i32_sequence() -> Datatype {
837        Datatype::VariableLength {
838            is_string: false,
839            base_type: Box::new(Datatype::FixedPoint {
840                size: 4,
841                signed: true,
842                byte_order: crate::datatype::DatatypeByteOrder::LittleEndian,
843                bit_offset: 0,
844                bit_precision: 32,
845            }),
846            padding: None,
847            charset: None,
848        }
849    }
850
851    fn i32_type() -> Datatype {
852        Datatype::FixedPoint {
853            size: 4,
854            signed: true,
855            byte_order: crate::datatype::DatatypeByteOrder::LittleEndian,
856            bit_offset: 0,
857            bit_precision: 32,
858        }
859    }
860
861    fn member(name: &str, byte_offset: u64, datatype: Datatype) -> CompoundMember {
862        CompoundMember {
863            name: name.to_string(),
864            byte_offset,
865            datatype,
866        }
867    }
868
869    #[test]
870    fn a_plain_datatype_reaches_no_reference() {
871        assert_eq!(embedded_vlen_slots(&i32_type()), Some(Vec::new()));
872    }
873
874    #[test]
875    fn a_top_level_vlen_is_its_own_slot() {
876        assert_eq!(
877            embedded_vlen_slots(&vlen_string()),
878            Some(vec![EmbeddedVlSlot {
879                byte_offset: 0,
880                element_size: 1
881            }])
882        );
883    }
884
885    /// Each member keeps its own base-type width: a string reference counts
886    /// bytes, a sequence reference counts base-type elements. Collapsing the two
887    /// would read the wrong number of heap bytes back.
888    #[test]
889    fn compound_members_keep_their_own_element_size() {
890        let dt = Datatype::Compound {
891            size: 40,
892            members: vec![
893                member("label", 0, vlen_string()),
894                member("id", 16, i32_type()),
895                member("samples", 24, vlen_i32_sequence()),
896            ],
897        };
898        assert_eq!(
899            embedded_vlen_slots(&dt),
900            Some(vec![
901                EmbeddedVlSlot {
902                    byte_offset: 0,
903                    element_size: 1
904                },
905                EmbeddedVlSlot {
906                    byte_offset: 24,
907                    element_size: 4
908                },
909            ])
910        );
911    }
912
913    /// A compound nested inside a compound, and an array of compounds, both reach
914    /// references that a single-level walk would miss.
915    #[test]
916    fn nested_compounds_and_arrays_are_walked() {
917        let inner = Datatype::Compound {
918            size: 20,
919            members: vec![member("id", 0, i32_type()), member("s", 4, vlen_string())],
920        };
921        let nested = Datatype::Compound {
922            size: 24,
923            members: vec![
924                member("n", 0, i32_type()),
925                member("inner", 4, inner.clone()),
926            ],
927        };
928        assert_eq!(
929            embedded_vlen_slots(&nested),
930            Some(vec![EmbeddedVlSlot {
931                byte_offset: 8,
932                element_size: 1
933            }])
934        );
935
936        let array = Datatype::Array {
937            base_type: Box::new(inner),
938            dimensions: vec![3],
939        };
940        assert_eq!(
941            embedded_vlen_slots(&array),
942            Some(vec![
943                EmbeddedVlSlot {
944                    byte_offset: 4,
945                    element_size: 1
946                },
947                EmbeddedVlSlot {
948                    byte_offset: 24,
949                    element_size: 1
950                },
951                EmbeddedVlSlot {
952                    byte_offset: 44,
953                    element_size: 1
954                },
955            ])
956        );
957    }
958
959    /// A datatype whose declared size cannot hold the references it declares is
960    /// rejected rather than walked: the element bytes and the datatype disagree,
961    /// so any offset derived from the latter would index the wrong place.
962    #[test]
963    fn a_datatype_too_small_for_its_own_references_is_rejected() {
964        let dt = Datatype::Compound {
965            size: 8, // one VL reference needs 16
966            members: vec![member("s", 0, vlen_string())],
967        };
968        assert_eq!(embedded_vlen_slots(&dt), None);
969    }
970
971    /// An array declaring dimensions whose product overflows must not drive an
972    /// unbounded walk; the capacity bound stops it and the result is rejected.
973    #[test]
974    fn an_array_declaring_absurd_dimensions_is_rejected_not_walked() {
975        let dt = Datatype::Array {
976            base_type: Box::new(vlen_string()),
977            dimensions: vec![u32::MAX, u32::MAX],
978        };
979        assert_eq!(embedded_vlen_slots(&dt), None);
980    }
981
982    /// Deeply nested arrays must cost time proportional to the slots they
983    /// produce, not exponential in nesting depth.
984    ///
985    /// Walking each entry from scratch recomputes the same sub-tree once per
986    /// entry at every level, so a datatype of ~13 bytes per level — trivially
987    /// small in an object header, and depth-unlimited in `Datatype::parse` — used
988    /// to burn seconds here and hours a few levels further down. Any repack of an
989    /// untrusted file reaches this walk.
990    #[test]
991    fn deeply_nested_arrays_cost_time_proportional_to_their_slots() {
992        let mut dt = vlen_string();
993        for _ in 0..17 {
994            dt = Datatype::Array {
995                base_type: Box::new(dt),
996                dimensions: vec![2],
997            };
998        }
999        let started = std::time::Instant::now();
1000        let slots = embedded_vlen_slots(&dt).expect("a well-formed nesting must be walkable");
1001        let elapsed = started.elapsed();
1002
1003        assert_eq!(slots.len(), 1 << 17, "one slot per leaf entry");
1004        // The linear walk does this in milliseconds even unoptimized; the
1005        // exponential one took seconds in release. A wide margin keeps the test
1006        // from being a CI-timing flake while still failing the regression by
1007        // orders of magnitude.
1008        assert!(
1009            elapsed < std::time::Duration::from_secs(10),
1010            "walking {} slots took {elapsed:?}; the walk is no longer linear in its output",
1011            slots.len()
1012        );
1013    }
1014
1015    /// A member offset is read from the file as a `u64`, so it can name a
1016    /// position no `usize` can address. Truncating it would land the slot
1017    /// somewhere plausible *inside* the element, where the size check below would
1018    /// wave it through and the rewrite would corrupt an unrelated field; the walk
1019    /// has to refuse instead. On 64-bit the offset simply exceeds the element.
1020    #[test]
1021    fn a_member_offset_beyond_the_address_space_is_rejected() {
1022        let dt = Datatype::Compound {
1023            size: 40,
1024            members: vec![member("s", u64::MAX - 8, vlen_string())],
1025        };
1026        assert_eq!(embedded_vlen_slots(&dt), None);
1027    }
1028}