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::FileSource;
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
173fn check_element_limit(
174    num_elements: u64,
175    options: VlenStringReadOptions,
176) -> Result<(), FormatError> {
177    if let Some(limit) = options.max_elements
178        && num_elements > limit as u64
179    {
180        return Err(FormatError::VariableLengthElementLimitExceeded {
181            limit,
182            actual: num_elements,
183        });
184    }
185    Ok(())
186}
187
188fn payload_size(refs: &[VlElement], options: VlenStringReadOptions) -> Result<u64, FormatError> {
189    let mut required = 0u64;
190    for element in refs {
191        required =
192            required
193                .checked_add(u64::from(element.length))
194                .ok_or(FormatError::OffsetOverflow {
195                    offset: required,
196                    length: u64::from(element.length),
197                })?;
198    }
199    if let Some(limit) = options.max_payload_bytes
200        && required > limit as u64
201    {
202        return Err(FormatError::VariableLengthByteLimitExceeded { limit, required });
203    }
204    Ok(required)
205}
206
207/// Return the total payload bytes named by a set of VL references.
208pub fn vlen_string_payload_size(
209    raw_data: &[u8],
210    num_elements: u64,
211    offset_size: u8,
212) -> Result<u64, FormatError> {
213    check_element_limit(num_elements, VlenStringReadOptions::default())?;
214    let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
215    payload_size(&refs, VlenStringReadOptions::default())
216}
217
218/// Resolve VL strings from a random-access file source and pass them to a
219/// visitor one at a time.
220pub fn visit_vl_strings_from_source<S, F>(
221    source: &S,
222    raw_data: &[u8],
223    num_elements: u64,
224    offset_size: u8,
225    length_size: u8,
226    base_address: u64,
227    options: VlenStringReadOptions,
228    mut visitor: F,
229) -> Result<(), FormatError>
230where
231    S: FileSource + ?Sized,
232    F: FnMut(&str),
233{
234    check_element_limit(num_elements, options)?;
235    let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
236    payload_size(&refs, options)?;
237
238    let mut collections: Vec<(u64, GlobalHeapIndex)> = Vec::new();
239    for element in &refs {
240        if element.length == 0
241            && (is_undefined_addr(element.collection_address, offset_size)
242                || element.collection_address == 0)
243        {
244            visitor("");
245            continue;
246        }
247        if is_undefined_addr(element.collection_address, offset_size) {
248            return Err(FormatError::VlDataError(
249                "non-empty VL element has an undefined heap address".into(),
250            ));
251        }
252
253        let collection_address = element.collection_address.checked_add(base_address).ok_or(
254            FormatError::OffsetOverflow {
255                offset: element.collection_address,
256                length: base_address,
257            },
258        )?;
259        let collection_pos = match collections
260            .iter()
261            .position(|(address, _)| *address == collection_address)
262        {
263            Some(pos) => pos,
264            None => {
265                let collection = GlobalHeapIndex::parse(source, collection_address, length_size)?;
266                collections.push((collection_address, collection));
267                collections.len() - 1
268            }
269        };
270
271        let index = u16::try_from(element.object_index).map_err(|_| {
272            FormatError::VlDataError(format!(
273                "global heap object index {} does not fit u16",
274                element.object_index
275            ))
276        })?;
277        let object = collections[collection_pos].1.get_object(index).ok_or(
278            FormatError::GlobalHeapObjectNotFound {
279                collection_address,
280                index,
281            },
282        )?;
283        if u64::from(element.length) > object.size {
284            return Err(FormatError::VlDataError(format!(
285                "VL element length {} exceeds global heap object size {}",
286                element.length, object.size
287            )));
288        }
289
290        let bytes = source.read_exact_at(object.data_address, element.length as usize)?;
291        let string = String::from_utf8_lossy(&bytes);
292        visitor(&string);
293    }
294
295    Ok(())
296}
297
298/// Resolve VL strings from a random-access file source.
299pub fn read_vl_strings_from_source<S: FileSource + ?Sized>(
300    source: &S,
301    raw_data: &[u8],
302    num_elements: u64,
303    offset_size: u8,
304    length_size: u8,
305    base_address: u64,
306    options: VlenStringReadOptions,
307) -> Result<Vec<String>, FormatError> {
308    let mut strings = Vec::new();
309    visit_vl_strings_from_source(
310        source,
311        raw_data,
312        num_elements,
313        offset_size,
314        length_size,
315        base_address,
316        options,
317        |string| strings.push(String::from(string)),
318    )?;
319    Ok(strings)
320}
321
322/// One element of a variable-length string dataset/attribute, read as exact
323/// heap bytes rather than a lossily-decoded `String`.
324///
325/// `None` is a *null* reference (length 0 with an undefined or zero heap
326/// address), which the HDF5 model distinguishes from an empty string. `Some`
327/// is a real heap object, carrying its exact bytes (possibly empty, possibly
328/// containing embedded NULs or non-UTF-8 sequences). Preserving this
329/// distinction lets a faithful rewrite reproduce the source byte-for-byte.
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub(crate) enum VlByteObject {
332    /// A null VL reference (no heap object).
333    Null,
334    /// A heap object holding these exact bytes.
335    Bytes(Vec<u8>),
336}
337
338/// Resolve a VL element's exact heap bytes from a random-access source,
339/// preserving the null-vs-empty distinction and never lossily decoding.
340///
341/// This mirrors [`visit_vl_strings_from_source`] but yields raw bytes (and a
342/// null marker) instead of a `&str`, so a faithful rewrite can reproduce
343/// embedded-NUL and non-UTF-8 payloads exactly.
344///
345/// `element_size` is the byte width of one base-type element of the sequence.
346/// For VL strings the base type is a single byte, so `element_size == 1` and the
347/// reference's stored `length` (an element count) equals the byte count. For a
348/// non-string VL sequence (e.g. `H5T_VLEN { H5T_NATIVE_DOUBLE }`) the stored
349/// `length` counts base-type elements, so the heap object holds
350/// `length * element_size` bytes — exactly what is read here.
351pub(crate) fn read_vl_byte_objects_from_source<S: FileSource + ?Sized>(
352    source: &S,
353    raw_data: &[u8],
354    num_elements: u64,
355    offset_size: u8,
356    length_size: u8,
357    base_address: u64,
358    element_size: usize,
359    options: VlenStringReadOptions,
360) -> Result<Vec<VlByteObject>, FormatError> {
361    check_element_limit(num_elements, options)?;
362    let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
363    payload_size(&refs, options)?;
364
365    let mut objects = Vec::with_capacity(refs.len());
366    let mut collections: Vec<(u64, GlobalHeapIndex)> = Vec::new();
367    for element in &refs {
368        if element.length == 0
369            && (is_undefined_addr(element.collection_address, offset_size)
370                || element.collection_address == 0)
371        {
372            objects.push(VlByteObject::Null);
373            continue;
374        }
375        if is_undefined_addr(element.collection_address, offset_size) {
376            return Err(FormatError::VlDataError(
377                "non-empty VL element has an undefined heap address".into(),
378            ));
379        }
380
381        let collection_address = element.collection_address.checked_add(base_address).ok_or(
382            FormatError::OffsetOverflow {
383                offset: element.collection_address,
384                length: base_address,
385            },
386        )?;
387        let collection_pos = match collections
388            .iter()
389            .position(|(address, _)| *address == collection_address)
390        {
391            Some(pos) => pos,
392            None => {
393                let collection = GlobalHeapIndex::parse(source, collection_address, length_size)?;
394                collections.push((collection_address, collection));
395                collections.len() - 1
396            }
397        };
398
399        let index = u16::try_from(element.object_index).map_err(|_| {
400            FormatError::VlDataError(format!(
401                "global heap object index {} does not fit u16",
402                element.object_index
403            ))
404        })?;
405        let object = collections[collection_pos].1.get_object(index).ok_or(
406            FormatError::GlobalHeapObjectNotFound {
407                collection_address,
408                index,
409            },
410        )?;
411        // The heap object holds `length` base-type elements of `element_size`
412        // bytes each. Compute the byte count with checked arithmetic so a hostile
413        // `length` cannot overflow, and bound it by the heap object's own size.
414        let byte_len = (element.length as u64)
415            .checked_mul(element_size as u64)
416            .ok_or(FormatError::OffsetOverflow {
417                offset: u64::from(element.length),
418                length: element_size as u64,
419            })?;
420        if byte_len > object.size {
421            return Err(FormatError::VlDataError(format!(
422                "VL element length {} ({} bytes) exceeds global heap object size {}",
423                element.length, byte_len, object.size
424            )));
425        }
426
427        let bytes = source.read_exact_at(object.data_address, byte_len.to_usize()?)?;
428        objects.push(VlByteObject::Bytes(bytes));
429    }
430
431    Ok(objects)
432}
433
434/// Resolve VL strings from an in-memory buffer by looking up each element in the
435/// global heap. A thin convenience wrapper over
436/// [`read_vl_strings_from_source`] used by the unit tests; production callers go
437/// straight to the source-based reader so a streaming backend works unchanged.
438#[cfg(test)]
439pub fn read_vl_strings(
440    file_data: &[u8],
441    raw_data: &[u8],
442    num_elements: u64,
443    offset_size: u8,
444    length_size: u8,
445) -> Result<Vec<String>, FormatError> {
446    read_vl_strings_from_source(
447        &BytesSource::new(file_data),
448        raw_data,
449        num_elements,
450        offset_size,
451        length_size,
452        0,
453        VlenStringReadOptions::default(),
454    )
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    /// Build a global heap collection at given offset in a file buffer.
462    fn build_gcol_at(
463        file_data: &mut Vec<u8>,
464        offset: usize,
465        objects: &[(u16, &[u8])], // (index, data)
466    ) {
467        let length_size = 8usize;
468
469        // Ensure file_data is large enough
470        let header_size = 8 + length_size;
471        let mut obj_total = 0usize;
472        for (_, data) in objects {
473            let padded = (data.len() + 7) & !7;
474            obj_total += 8 + length_size + padded;
475        }
476        obj_total += 2; // free space marker
477        let collection_size = header_size + obj_total;
478        let needed = offset + collection_size;
479        if file_data.len() < needed {
480            file_data.resize(needed, 0);
481        }
482
483        let mut pos = offset;
484        // Signature
485        file_data[pos..pos + 4].copy_from_slice(b"GCOL");
486        file_data[pos + 4] = 1; // version
487        // reserved(3) already 0
488        pos += 8;
489        file_data[pos..pos + 8].copy_from_slice(&(collection_size as u64).to_le_bytes());
490        pos += 8;
491
492        for (index, data) in objects {
493            file_data[pos..pos + 2].copy_from_slice(&index.to_le_bytes());
494            file_data[pos + 2..pos + 4].copy_from_slice(&1u16.to_le_bytes()); // ref_count
495            // reserved(4) already 0
496            pos += 8;
497            file_data[pos..pos + 8].copy_from_slice(&(data.len() as u64).to_le_bytes());
498            pos += 8;
499            file_data[pos..pos + data.len()].copy_from_slice(data);
500            let padded = (data.len() + 7) & !7;
501            pos += padded;
502        }
503        // free space marker
504        file_data[pos..pos + 2].copy_from_slice(&0u16.to_le_bytes());
505    }
506
507    /// Build VL reference raw data for given strings at a collection address.
508    fn build_vl_refs(
509        strings: &[&str],
510        collection_address: u64,
511        start_index: u16,
512        offset_size: u8,
513    ) -> Vec<u8> {
514        let mut raw = Vec::new();
515        for (i, s) in strings.iter().enumerate() {
516            raw.extend_from_slice(&(s.len() as u32).to_le_bytes());
517            match offset_size {
518                4 => raw.extend_from_slice(&(collection_address as u32).to_le_bytes()),
519                8 => raw.extend_from_slice(&collection_address.to_le_bytes()),
520                _ => panic!("unsupported"),
521            }
522            raw.extend_from_slice(&(start_index as u32 + i as u32).to_le_bytes());
523        }
524        raw
525    }
526
527    #[test]
528    fn parse_vl_references_two_elements() {
529        let raw = build_vl_refs(&["hello", "world"], 0x1000, 1, 8);
530        let refs = parse_vl_references(&raw, 2, 8).unwrap();
531        assert_eq!(refs.len(), 2);
532        assert_eq!(refs[0].length, 5);
533        assert_eq!(refs[0].collection_address, 0x1000);
534        assert_eq!(refs[0].object_index, 1);
535        assert_eq!(refs[1].length, 5);
536        assert_eq!(refs[1].object_index, 2);
537    }
538
539    #[test]
540    fn read_vl_strings_from_heap() {
541        let gcol_offset = 256usize;
542        let mut file_data = vec![0u8; 512];
543        build_gcol_at(&mut file_data, gcol_offset, &[(1, b"Alice"), (2, b"Bob")]);
544
545        let raw = build_vl_refs(&["Alice", "Bob"], gcol_offset as u64, 1, 8);
546        let strings = read_vl_strings(&file_data, &raw, 2, 8, 8).unwrap();
547        assert_eq!(strings, vec!["Alice", "Bob"]);
548    }
549
550    #[cfg(feature = "std")]
551    #[test]
552    fn read_vl_strings_from_seekable_source() {
553        use std::io::Cursor;
554
555        use crate::source::ReadSeekSource;
556
557        let gcol_offset = 256usize;
558        let mut file_data = vec![0u8; 512];
559        build_gcol_at(&mut file_data, gcol_offset, &[(1, b"Alice"), (2, b"Bob")]);
560        let raw = build_vl_refs(&["Alice", "Bob"], gcol_offset as u64, 1, 8);
561        let source = ReadSeekSource::new(Cursor::new(file_data)).unwrap();
562
563        let strings = read_vl_strings_from_source(
564            &source,
565            &raw,
566            2,
567            8,
568            8,
569            0,
570            VlenStringReadOptions::default(),
571        )
572        .unwrap();
573        assert_eq!(strings, vec!["Alice", "Bob"]);
574    }
575
576    #[test]
577    fn null_vl_element_empty_string() {
578        // length=0, address=undefined
579        let mut raw = Vec::new();
580        raw.extend_from_slice(&0u32.to_le_bytes()); // length=0
581        raw.extend_from_slice(&u64::MAX.to_le_bytes()); // undefined address
582        raw.extend_from_slice(&0u32.to_le_bytes()); // index
583
584        let file_data = vec![0u8; 16];
585        let strings = read_vl_strings(&file_data, &raw, 1, 8, 8).unwrap();
586        assert_eq!(strings, vec![""]);
587    }
588
589    #[test]
590    fn null_vl_element_zero_address() {
591        let mut raw = Vec::new();
592        raw.extend_from_slice(&0u32.to_le_bytes());
593        raw.extend_from_slice(&0u64.to_le_bytes());
594        raw.extend_from_slice(&0u32.to_le_bytes());
595
596        let file_data = vec![0u8; 16];
597        let strings = read_vl_strings(&file_data, &raw, 1, 8, 8).unwrap();
598        assert_eq!(strings, vec![""]);
599    }
600
601    #[test]
602    fn parse_vl_references_truncated_error() {
603        let raw = vec![0u8; 10]; // too short for 1 element with offset_size=8
604        let err = parse_vl_references(&raw, 1, 8).unwrap_err();
605        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
606    }
607}