//! Variable-length data reading (VL strings & VL sequences). //! //! VL data elements in HDF5 store their values in the global heap. //! The raw data for each element contains a global heap ID: //! `sequence_length(4 LE) + collection_address(offset_size LE) + object_index(4 LE)`. #[cfg(not(feature = "std"))] use alloc::{collections::BTreeMap, format, string::String, vec, vec::Vec}; #[cfg(feature = "std")] use std::collections::BTreeMap; use crate::error::FormatError; use crate::global_heap::{GlobalHeapCollection, GlobalHeapIndex}; /// A parsed variable-length element reference (global heap ID). #[derive(Debug, Clone)] pub struct VlElement { /// Length of the VL data. pub length: u32, /// Address of the global heap collection containing the data. pub collection_address: u64, /// Index of the object within the collection. pub object_index: u32, } fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> { match offset.checked_add(needed) { Some(end) if end <= data.len() => Ok(()), _ => Err(FormatError::UnexpectedEof { expected: offset.saturating_add(needed), available: data.len(), }), } } fn read_offset(data: &[u8], pos: usize, offset_size: u8) -> Result { let s = offset_size as usize; ensure_len(data, pos, s)?; let slice = &data[pos..pos + s]; Ok(match offset_size { 2 => u16::from_le_bytes([slice[0], slice[1]]) as u64, 4 => u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as u64, 8 => u64::from_le_bytes([ slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], ]), _ => return Err(FormatError::InvalidOffsetSize(offset_size)), }) } /// Parse VL global heap references from raw attribute/dataset data. pub fn parse_vl_references( raw_data: &[u8], num_elements: u64, offset_size: u8, ) -> Result, FormatError> { let elem_size = 4 + offset_size as usize + 4; // length + address + index let total = (num_elements as usize) .checked_mul(elem_size) .ok_or(FormatError::UnexpectedEof { expected: usize::MAX, available: raw_data.len(), })?; if raw_data.len() < total { return Err(FormatError::UnexpectedEof { expected: total, available: raw_data.len(), }); } let mut elements = Vec::with_capacity(num_elements as usize); let mut pos = 0; for _ in 0..num_elements { let length = u32::from_le_bytes([ raw_data[pos], raw_data[pos + 1], raw_data[pos + 2], raw_data[pos + 3], ]); pos += 4; let collection_address = read_offset(raw_data, pos, offset_size)?; pos += offset_size as usize; let object_index = u32::from_le_bytes([ raw_data[pos], raw_data[pos + 1], raw_data[pos + 2], raw_data[pos + 3], ]); pos += 4; elements.push(VlElement { length, collection_address, object_index, }); } Ok(elements) } /// Check if an address represents an undefined/null address. fn is_undefined_address(addr: u64, offset_size: u8) -> bool { match offset_size { 2 => addr == 0xFFFF, 4 => addr == 0xFFFF_FFFF, 8 => addr == 0xFFFF_FFFF_FFFF_FFFF, _ => false, } } /// The size of one variable-length element in a file with `offset_size`-byte /// addresses: a sequence length (4), a global heap collection address and an /// object index (4). libhdf5 computes it this way rather than trusting the /// datatype message (`H5T_set_loc`). pub fn element_size(offset_size: u8) -> usize { 4 + offset_size as usize + 4 } /// Refuse a variable-length datatype whose stored element size is not the /// one this file's offset size implies. Its elements would be laid out with /// a stride libhdf5 does not use, so every value after the first would be /// read from the wrong place. pub fn check_element_size(stored_size: u32, offset_size: u8) -> Result<(), FormatError> { let expected = element_size(offset_size); if stored_size as usize != expected { return Err(FormatError::VlDataError(format!( "variable-length datatype stores {stored_size}-byte elements; a file with \ {offset_size}-byte offsets uses {expected}" ))); } Ok(()) } /// A collection's objects, located in the file data but not copied: /// `(index, offset, size)` of the first object with each index, sorted by /// index. struct CachedCollection { objects: Vec<(u16, usize, usize)>, } impl CachedCollection { fn new(index: GlobalHeapIndex) -> Self { let mut objects: Vec<(u16, usize, usize)> = index .objects .iter() .map(|o| (o.index, o.offset, o.size)) .collect(); // Stable, so the first object with a repeated index is kept. objects.sort_by_key(|o| o.0); objects.dedup_by_key(|o| o.0); Self { objects } } /// What this entry costs to keep, in bytes (roughly). fn cost(&self) -> usize { 64 + self.objects.len() * core::mem::size_of::<(u16, usize, usize)>() } fn get(&self, index: u32) -> Option<(usize, usize)> { let index = u16::try_from(index).ok()?; let i = self.objects.binary_search_by_key(&index, |o| o.0).ok()?; Some((self.objects[i].1, self.objects[i].2)) } } /// How many bytes of collection indexes a [`VlResolver`] keeps before it /// drops them and starts again. Values are never copied into the cache, so /// this bounds what a read retains however many collections it visits. const CACHE_BUDGET: usize = 32 << 20; /// Resolves variable-length elements against a file's global heap, parsing /// each heap collection once however many elements point into it. /// /// Values follow libhdf5: an element whose heap address is 0 is null (an /// empty string or sequence), and an element whose heap object is not /// exactly `length × base size` bytes is an error ("Expected global heap /// object size does not match"), not a truncated or padded value. /// /// Memory stays bounded on hostile files: the cache holds where each /// object lies, not a copy of it, up to a fixed budget; and collections /// that overlap one another are refused (libhdf5 never writes them), so a /// file cannot make the resolver parse the same bytes as the objects of /// many collections. pub struct VlResolver<'a> { file_data: &'a [u8], offset_size: u8, length_size: u8, cache: BTreeMap, cached_bytes: usize, budget: usize, /// Start → end of every collection parsed so far (kept when the cache /// is dropped, to check overlaps). extents: BTreeMap, } impl<'a> VlResolver<'a> { /// A resolver over `file_data` (the file from its superblock on), with /// the superblock's offset and length sizes. pub fn new(file_data: &'a [u8], offset_size: u8, length_size: u8) -> Self { Self { file_data, offset_size, length_size, cache: BTreeMap::new(), cached_bytes: 0, budget: CACHE_BUDGET, extents: BTreeMap::new(), } } /// The size of one element in this file (see [`element_size`]). pub fn element_size(&self) -> usize { element_size(self.offset_size) } /// Split `raw` into elements; its length must be a whole number of them. fn elements(&self, raw: &[u8]) -> Result, FormatError> { let size = self.element_size(); if !raw.len().is_multiple_of(size) { return Err(FormatError::VlDataError(format!( "{} bytes is not a whole number of {size}-byte variable-length elements", raw.len() ))); } parse_vl_references(raw, (raw.len() / size) as u64, self.offset_size) } /// The bytes of one element: `length × base_size` bytes from the heap, /// or `None` for a null element. fn resolve( &mut self, vl: &VlElement, base_size: usize, ) -> Result, FormatError> { let addr = vl.collection_address; if addr == 0 { return Ok(None); } if vl.length == 0 && is_undefined_address(addr, self.offset_size) { return Ok(Some(&[])); } let data = self.object(vl)?; let expected = (vl.length as usize) .checked_mul(base_size) .ok_or_else(|| FormatError::Overflow("variable-length element size".into()))?; if data.len() != expected { return Err(FormatError::VlDataError(format!( "global heap object {} in the collection at {addr} holds {} bytes; the element \ says {} × {base_size}", vl.object_index, data.len(), vl.length ))); } Ok(Some(data)) } /// One element (the first [`element_size`](Self::element_size) bytes of /// `elem`) of a variable-length sequence whose base type is `base_size` /// bytes: its `length × base_size` bytes, or `None` for a null element /// (heap address 0). pub fn element( &mut self, elem: &[u8], base_size: usize, ) -> Result, FormatError> { let vl = parse_vl_references(elem, 1, self.offset_size)?; self.resolve(&vl[0], base_size) } /// One variable-length string element: its bytes up to the first NUL, /// or `None` for a null element (h5dump prints it as `NULL`, h5py /// returns it as empty). pub fn string_element(&mut self, elem: &[u8]) -> Result, FormatError> { Ok(self.element(elem, 1)?.map(cut_at_nul)) } /// The strings of the variable-length string elements in `raw`, as /// bytes. A string ends at its first NUL, as libhdf5 returns it (it /// converts each to a C string); a null element is empty. pub fn string_bytes(&mut self, raw: &[u8]) -> Result>, FormatError> { self.elements(raw)? .iter() .map(|vl| Ok(self.resolve(vl, 1)?.map(cut_at_nul).unwrap_or(&[]).to_vec())) .collect() } /// The strings of the variable-length string elements in `raw`, decoded /// as UTF-8 with invalid sequences replaced by U+FFFD (see /// [`string_bytes`](Self::string_bytes) for the exact bytes). pub fn strings(&mut self, raw: &[u8]) -> Result, FormatError> { Ok(self .string_bytes(raw)? .into_iter() .map(|b| match String::from_utf8(b) { Ok(s) => s, Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(), }) .collect()) } /// The sequences of the variable-length sequence elements in `raw`, each /// as its `length × base_size` bytes in the base type's encoding. pub fn sequences(&mut self, raw: &[u8], base_size: usize) -> Result>, FormatError> { if base_size == 0 { return Err(FormatError::VlDataError( "variable-length sequence of a zero-size base type".into(), )); } self.elements(raw)? .iter() .map(|vl| Ok(self.resolve(vl, base_size)?.unwrap_or(&[]).to_vec())) .collect() } } /// A string's bytes up to its first NUL. fn cut_at_nul(s: &[u8]) -> &[u8] { &s[..s.iter().position(|&b| b == 0).unwrap_or(s.len())] } /// Resolve VL strings from raw data by looking up each element in the global heap. /// /// Reads the first `num_elements` elements of `raw`. Strings end at their /// first NUL and invalid UTF-8 is replaced, as in [`VlResolver::strings`]. pub fn read_vl_strings( file_data: &[u8], raw_data: &[u8], num_elements: u64, offset_size: u8, length_size: u8, ) -> Result, FormatError> { let raw = first_elements(raw_data, num_elements, offset_size)?; VlResolver::new(file_data, offset_size, length_size).strings(raw) } /// The first `num_elements` elements of `raw`, or an error if it is shorter. fn first_elements(raw: &[u8], num_elements: u64, offset_size: u8) -> Result<&[u8], FormatError> { let total = usize::try_from(num_elements) .ok() .and_then(|n| n.checked_mul(element_size(offset_size))) .ok_or(FormatError::UnexpectedEof { expected: usize::MAX, available: raw.len(), })?; raw.get(..total).ok_or(FormatError::UnexpectedEof { expected: total, available: raw.len(), }) } /// Resolve VL sequences from raw data, returning each element's bytes. /// /// Each element is the sequence's full encoding — element count × base type /// size bytes, in the base type's byte order — so a sequence of `i32` yields /// four bytes per value. Decode it with the base type (e.g. /// [`crate::data_read::read_as_i64`]). This does not know the base type, so /// it returns each heap object whole; [`VlResolver::sequences`] also checks /// the object's size against the element's length. pub fn read_vl_bytes( file_data: &[u8], raw_data: &[u8], num_elements: u64, offset_size: u8, length_size: u8, ) -> Result>, FormatError> { let refs = parse_vl_references(raw_data, num_elements, offset_size)?; let mut resolver = VlResolver::new(file_data, offset_size, length_size); let mut result = Vec::with_capacity(refs.len()); for vl in &refs { if vl.length == 0 && (is_undefined_address(vl.collection_address, offset_size) || vl.collection_address == 0) { result.push(Vec::new()); continue; } // The heap object holds the whole sequence. `vl.length` counts // elements, not bytes, so it is only the byte length when the base // type is one byte wide. let obj = resolver.object(vl)?; result.push(obj.to_vec()); } Ok(result) } impl<'a> VlResolver<'a> { /// The heap object `vl` points to, whatever its size; its collection is /// parsed on first use. fn object(&mut self, vl: &VlElement) -> Result<&'a [u8], FormatError> { let addr = vl.collection_address; if !self.cache.contains_key(&addr) { let offset = usize::try_from(addr).map_err(|_| FormatError::UnexpectedEof { expected: usize::MAX, available: self.file_data.len(), })?; let index = GlobalHeapCollection::parse_index(self.file_data, offset, self.length_size)?; // parse_index checked that the collection lies in the file. let end = offset + index.collection_size as usize; self.check_overlap(offset, end)?; let coll = CachedCollection::new(index); if self.cached_bytes.saturating_add(coll.cost()) > self.budget { self.cache.clear(); self.cached_bytes = 0; } self.cached_bytes += coll.cost(); self.cache.insert(addr, coll); } let (start, size) = self.cache[&addr].get(vl.object_index).ok_or( FormatError::GlobalHeapObjectNotFound { collection_address: addr, index: vl.object_index as u16, }, )?; Ok(&self.file_data[start..start + size]) } /// Record the collection at `start..end`, refusing one that overlaps a /// collection already read. libhdf5 allocates each collection its own /// block; overlapping ones only come from a crafted file, where they let /// every byte be parsed again as the objects of each collection. fn check_overlap(&mut self, start: usize, end: usize) -> Result<(), FormatError> { if let Some(&known) = self.extents.get(&start) { return if known == end { Ok(()) } else { Err(FormatError::VlDataError(format!( "global heap collection at {start} changed size" ))) }; } let before = self.extents.range(..start).next_back(); let after = self.extents.range(start..).next(); let clash = match (before, after) { (Some((&s, &e)), _) if e > start => Some(s), (_, Some((&s, _))) if s < end => Some(s), _ => None, }; if let Some(other) = clash { return Err(FormatError::VlDataError(format!( "global heap collection at {start} overlaps the one at {other}" ))); } self.extents.insert(start, end); Ok(()) } } #[cfg(test)] mod tests { use super::*; /// Build a global heap collection at given offset in a file buffer. fn build_gcol_at( file_data: &mut Vec, offset: usize, objects: &[(u16, &[u8])], // (index, data) ) { let length_size = 8usize; // Ensure file_data is large enough let header_size = 8 + length_size; let mut obj_total = 0usize; for (_, data) in objects { let padded = (data.len() + 7) & !7; obj_total += 8 + length_size + padded; } obj_total += 2; // free space marker let collection_size = header_size + obj_total; let needed = offset + collection_size; if file_data.len() < needed { file_data.resize(needed, 0); } let mut pos = offset; // Signature file_data[pos..pos + 4].copy_from_slice(b"GCOL"); file_data[pos + 4] = 1; // version // reserved(3) already 0 pos += 8; file_data[pos..pos + 8].copy_from_slice(&(collection_size as u64).to_le_bytes()); pos += 8; for (index, data) in objects { file_data[pos..pos + 2].copy_from_slice(&index.to_le_bytes()); file_data[pos + 2..pos + 4].copy_from_slice(&1u16.to_le_bytes()); // ref_count // reserved(4) already 0 pos += 8; file_data[pos..pos + 8].copy_from_slice(&(data.len() as u64).to_le_bytes()); pos += 8; file_data[pos..pos + data.len()].copy_from_slice(data); let padded = (data.len() + 7) & !7; pos += padded; } // free space marker file_data[pos..pos + 2].copy_from_slice(&0u16.to_le_bytes()); } /// Build VL reference raw data for given strings at a collection address. fn build_vl_refs( strings: &[&str], collection_address: u64, start_index: u16, offset_size: u8, ) -> Vec { let mut raw = Vec::new(); for (i, s) in strings.iter().enumerate() { raw.extend_from_slice(&(s.len() as u32).to_le_bytes()); match offset_size { 4 => raw.extend_from_slice(&(collection_address as u32).to_le_bytes()), 8 => raw.extend_from_slice(&collection_address.to_le_bytes()), _ => panic!("unsupported"), } raw.extend_from_slice(&(start_index as u32 + i as u32).to_le_bytes()); } raw } #[test] fn parse_vl_references_two_elements() { let raw = build_vl_refs(&["hello", "world"], 0x1000, 1, 8); let refs = parse_vl_references(&raw, 2, 8).unwrap(); assert_eq!(refs.len(), 2); assert_eq!(refs[0].length, 5); assert_eq!(refs[0].collection_address, 0x1000); assert_eq!(refs[0].object_index, 1); assert_eq!(refs[1].length, 5); assert_eq!(refs[1].object_index, 2); } #[test] fn read_vl_strings_from_heap() { let gcol_offset = 256usize; let mut file_data = vec![0u8; 512]; build_gcol_at(&mut file_data, gcol_offset, &[(1, b"Alice"), (2, b"Bob")]); let raw = build_vl_refs(&["Alice", "Bob"], gcol_offset as u64, 1, 8); let strings = read_vl_strings(&file_data, &raw, 2, 8, 8).unwrap(); assert_eq!(strings, vec!["Alice", "Bob"]); } #[test] fn null_vl_element_empty_string() { // length=0, address=undefined let mut raw = Vec::new(); raw.extend_from_slice(&0u32.to_le_bytes()); // length=0 raw.extend_from_slice(&u64::MAX.to_le_bytes()); // undefined address raw.extend_from_slice(&0u32.to_le_bytes()); // index let file_data = vec![0u8; 16]; let strings = read_vl_strings(&file_data, &raw, 1, 8, 8).unwrap(); assert_eq!(strings, vec![""]); } #[test] fn null_vl_element_zero_address() { let mut raw = Vec::new(); raw.extend_from_slice(&0u32.to_le_bytes()); raw.extend_from_slice(&0u64.to_le_bytes()); raw.extend_from_slice(&0u32.to_le_bytes()); let file_data = vec![0u8; 16]; let strings = read_vl_strings(&file_data, &raw, 1, 8, 8).unwrap(); assert_eq!(strings, vec![""]); } #[test] fn read_vl_bytes_from_heap() { let gcol_offset = 128usize; let mut file_data = vec![0u8; 512]; build_gcol_at( &mut file_data, gcol_offset, &[(1, &[0xDE, 0xAD]), (2, &[0xBE, 0xEF, 0xCA])], ); let _raw = build_vl_refs(&["ab", "abc"], gcol_offset as u64, 1, 8); // Fix lengths to match actual byte lengths let mut raw_fixed = Vec::new(); raw_fixed.extend_from_slice(&2u32.to_le_bytes()); raw_fixed.extend_from_slice(&(gcol_offset as u64).to_le_bytes()); raw_fixed.extend_from_slice(&1u32.to_le_bytes()); raw_fixed.extend_from_slice(&3u32.to_le_bytes()); raw_fixed.extend_from_slice(&(gcol_offset as u64).to_le_bytes()); raw_fixed.extend_from_slice(&2u32.to_le_bytes()); let bytes = read_vl_bytes(&file_data, &raw_fixed, 2, 8, 8).unwrap(); assert_eq!(bytes, vec![vec![0xDE, 0xAD], vec![0xBE, 0xEF, 0xCA]]); } fn element(length: u32, addr: u64, index: u32, offset_size: u8) -> Vec { let mut raw = length.to_le_bytes().to_vec(); raw.extend_from_slice(&addr.to_le_bytes()[..offset_size as usize]); raw.extend_from_slice(&index.to_le_bytes()); raw } #[test] fn strings_end_at_the_first_nul() { // libhdf5 hands each VL string over as a C string, so h5py sees // "a\0b" as "a"; we used to return the NUL and what followed. let mut file_data = vec![0u8; 512]; build_gcol_at(&mut file_data, 64, &[(1, b"a\0b"), (2, b"cd")]); let mut raw = element(3, 64, 1, 8); raw.extend(element(2, 64, 2, 8)); let mut r = VlResolver::new(&file_data, 8, 8); assert_eq!( r.string_bytes(&raw).unwrap(), vec![b"a".to_vec(), b"cd".to_vec()] ); assert_eq!( read_vl_strings(&file_data, &raw, 2, 8, 8).unwrap(), ["a", "cd"] ); } #[test] fn a_heap_object_of_the_wrong_size_is_an_error() { // libhdf5: "Expected global heap object size does not match". We // used to return the object cut to the element's length. let mut file_data = vec![0u8; 512]; build_gcol_at(&mut file_data, 64, &[(1, b"cdefgh"), (2, &[1, 0, 0, 0])]); let mut r = VlResolver::new(&file_data, 8, 8); assert!(r.string_bytes(&element(3, 64, 1, 8)).is_err()); assert!(r.string_bytes(&element(9, 64, 1, 8)).is_err()); assert!(read_vl_strings(&file_data, &element(3, 64, 1, 8), 1, 8, 8).is_err()); // A sequence of one i32 is 4 bytes; of two, 8. assert_eq!( r.sequences(&element(1, 64, 2, 8), 4).unwrap(), vec![vec![1, 0, 0, 0]] ); assert!(r.sequences(&element(2, 64, 2, 8), 4).is_err()); assert!(r.sequences(&element(1, 64, 2, 8), 0).is_err()); } #[test] fn address_zero_is_null_whatever_the_length() { // libhdf5 treats a heap address of 0 as a null element. let file_data = vec![0u8; 64]; let mut r = VlResolver::new(&file_data, 8, 8); assert_eq!( r.string_bytes(&element(5, 0, 1, 8)).unwrap(), vec![Vec::::new()] ); assert_eq!( r.sequences(&element(5, 0, 1, 8), 4).unwrap(), vec![Vec::::new()] ); } #[test] fn four_byte_offsets_use_twelve_byte_elements() { let mut file_data = vec![0u8; 512]; build_gcol_at(&mut file_data, 64, &[(1, b"one"), (2, b""), (3, b"three")]); let mut raw = element(3, 64, 1, 4); raw.extend(element(0, 64, 2, 4)); raw.extend(element(5, 64, 3, 4)); assert_eq!(raw.len(), 36); let mut r = VlResolver::new(&file_data, 4, 8); assert_eq!(r.element_size(), 12); assert_eq!(r.strings(&raw).unwrap(), ["one", "", "three"]); // Not a whole number of elements. assert!(r.strings(&raw[..30]).is_err()); } #[test] fn the_cache_stays_within_its_budget_and_rereads_what_it_dropped() { // Twenty collections of three objects each; a budget that holds // about two of them. Reading every element twice must still return // the right strings after the cache is dropped. let mut file_data = vec![0u8; 64]; let mut raw = Vec::new(); for c in 0..20u64 { let at = file_data.len(); let names: Vec = (0..3).map(|i| format!("c{c}o{i}")).collect(); let objs: Vec<(u16, &[u8])> = names .iter() .enumerate() .map(|(i, n)| (i as u16 + 1, n.as_bytes())) .collect(); build_gcol_at(&mut file_data, at, &objs); for (i, n) in names.iter().enumerate() { raw.extend(element(n.len() as u32, at as u64, i as u32 + 1, 8)); } } raw.extend(raw.clone()); let mut r = VlResolver::new(&file_data, 8, 8); let one = CachedCollection { objects: vec![(0, 0, 0); 3], } .cost(); r.budget = 2 * one + 1; let want: Vec = (0..2) .flat_map(|_| (0..20).flat_map(|c| (0..3).map(move |i| format!("c{c}o{i}")))) .collect(); for (k, chunk) in raw.chunks(16).enumerate() { assert_eq!(r.strings(chunk).unwrap(), [want[k].clone()]); assert!(r.cached_bytes <= r.budget); assert!(r.cache.len() <= 2); } } #[test] fn element_size_is_checked_against_the_offset_size() { assert!(check_element_size(16, 8).is_ok()); assert!(check_element_size(12, 4).is_ok()); assert!(check_element_size(16, 4).is_err()); assert!(check_element_size(524_304, 8).is_err()); } #[test] fn parse_vl_references_truncated_error() { let raw = vec![0u8; 10]; // too short for 1 element with offset_size=8 let err = parse_vl_references(&raw, 1, 8).unwrap_err(); assert!(matches!(err, FormatError::UnexpectedEof { .. })); } }