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