#[cfg(not(feature = "std"))]
use alloc::{format, string::String, vec::Vec};
use crate::convert::{TryToUsize, is_undefined_addr};
use crate::datatype::{CharacterSet, Datatype};
use crate::error::FormatError;
use crate::global_heap::GlobalHeapIndex;
#[cfg(test)]
use crate::source::BytesSource;
use crate::source::Source;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct VlenStringReadOptions {
max_elements: Option<usize>,
max_payload_bytes: Option<usize>,
}
impl VlenStringReadOptions {
pub const fn new() -> Self {
Self {
max_elements: None,
max_payload_bytes: None,
}
}
pub const fn with_max_elements(mut self, max_elements: usize) -> Self {
self.max_elements = Some(max_elements);
self
}
pub const fn with_max_payload_bytes(mut self, max_payload_bytes: usize) -> Self {
self.max_payload_bytes = Some(max_payload_bytes);
self
}
pub const fn max_elements(&self) -> Option<usize> {
self.max_elements
}
pub const fn max_payload_bytes(&self) -> Option<usize> {
self.max_payload_bytes
}
}
#[derive(Debug, Clone)]
pub struct VlElement {
pub length: u32,
pub collection_address: u64,
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<u64, FormatError> {
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)),
})
}
pub fn parse_vl_references(
raw_data: &[u8],
num_elements: u64,
offset_size: u8,
) -> Result<Vec<VlElement>, FormatError> {
let elem_size = 4 + offset_size as u64 + 4; let total = num_elements
.checked_mul(elem_size)
.ok_or(FormatError::OffsetOverflow {
offset: num_elements,
length: elem_size,
})?
.to_usize()?;
if raw_data.len() < total {
return Err(FormatError::UnexpectedEof {
expected: total,
available: raw_data.len(),
});
}
let mut elements = Vec::with_capacity(num_elements.to_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)
}
pub(crate) fn is_vlen_string_datatype(datatype: &Datatype) -> bool {
match datatype {
Datatype::VariableLength {
is_string: true, ..
} => true,
Datatype::VariableLength {
is_string: false,
base_type,
..
} => matches!(
base_type.as_ref(),
Datatype::String {
size: 1,
charset: CharacterSet::Ascii,
..
}
),
_ => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct EmbeddedVlSlot {
pub byte_offset: usize,
pub element_size: usize,
}
pub(crate) struct EmbeddedVlData {
pub raw: Vec<u8>,
pub offsets: Vec<usize>,
pub objects: Vec<VlByteObject>,
}
pub(crate) fn embedded_vlen_slots(datatype: &Datatype) -> Option<Vec<EmbeddedVlSlot>> {
let element_size = datatype.type_size() as usize;
let capacity = element_size / VL_REF_BYTES;
let mut slots = Vec::new();
if !collect_vlen_slots(datatype, 0, capacity, &mut slots) {
return None;
}
if slots.len() > capacity
|| slots.iter().any(|s| {
s.byte_offset
.checked_add(VL_REF_BYTES)
.is_none_or(|end| end > element_size)
})
{
return None;
}
Some(slots)
}
const VL_REF_BYTES: usize = 16;
fn collect_vlen_slots(
datatype: &Datatype,
base: usize,
capacity: usize,
out: &mut Vec<EmbeddedVlSlot>,
) -> bool {
if out.len() > capacity {
return true;
}
match datatype {
Datatype::VariableLength { base_type, .. } => {
let element_size = if is_vlen_string_datatype(datatype) {
1
} else {
(base_type.type_size() as usize).max(1)
};
out.push(EmbeddedVlSlot {
byte_offset: base,
element_size,
});
true
}
Datatype::Compound { members, .. } => {
for m in members {
let Some(at) = usize::try_from(m.byte_offset)
.ok()
.and_then(|off| base.checked_add(off))
else {
return false;
};
if !collect_vlen_slots(&m.datatype, at, capacity, out) {
return false;
}
}
true
}
Datatype::Array {
base_type,
dimensions,
} => {
let mut probe = Vec::new();
if !collect_vlen_slots(base_type, 0, capacity, &mut probe) {
return false;
}
if probe.is_empty() {
return true;
}
let count = dimensions
.iter()
.copied()
.fold(1u64, |a, b| a.saturating_mul(u64::from(b)));
if count > capacity as u64 {
return false;
}
let entries = usize::try_from(count).unwrap_or(usize::MAX);
let stride = base_type.type_size() as usize;
for i in 0..entries {
let Some(at) = i.checked_mul(stride).and_then(|off| base.checked_add(off)) else {
return false;
};
for slot in &probe {
let Some(byte_offset) = at.checked_add(slot.byte_offset) else {
return false;
};
out.push(EmbeddedVlSlot {
byte_offset,
element_size: slot.element_size,
});
if out.len() > capacity {
return true;
}
}
}
true
}
_ => true,
}
}
fn check_element_limit(
num_elements: u64,
options: VlenStringReadOptions,
) -> Result<(), FormatError> {
if let Some(limit) = options.max_elements
&& num_elements > limit as u64
{
return Err(FormatError::VariableLengthElementLimitExceeded {
limit,
actual: num_elements,
});
}
Ok(())
}
fn payload_size(refs: &[VlElement], options: VlenStringReadOptions) -> Result<u64, FormatError> {
let mut required = 0u64;
for element in refs {
required =
required
.checked_add(u64::from(element.length))
.ok_or(FormatError::OffsetOverflow {
offset: required,
length: u64::from(element.length),
})?;
}
if let Some(limit) = options.max_payload_bytes
&& required > limit as u64
{
return Err(FormatError::VariableLengthByteLimitExceeded { limit, required });
}
Ok(required)
}
pub fn vlen_string_payload_size(
raw_data: &[u8],
num_elements: u64,
offset_size: u8,
) -> Result<u64, FormatError> {
check_element_limit(num_elements, VlenStringReadOptions::default())?;
let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
payload_size(&refs, VlenStringReadOptions::default())
}
pub fn visit_vl_strings_from_source<S, F>(
source: &S,
raw_data: &[u8],
num_elements: u64,
offset_size: u8,
length_size: u8,
base_address: u64,
options: VlenStringReadOptions,
mut visitor: F,
) -> Result<(), FormatError>
where
S: Source + ?Sized,
F: FnMut(&str),
{
check_element_limit(num_elements, options)?;
let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
payload_size(&refs, options)?;
let mut wanted: Vec<(u64, Vec<u16>)> = Vec::new();
for element in &refs {
if is_undefined_addr(element.collection_address, offset_size)
|| (element.length == 0 && element.collection_address == 0)
{
continue;
}
let Some(address) = element.collection_address.checked_add(base_address) else {
continue;
};
let Ok(index) = u16::try_from(element.object_index) else {
continue;
};
match wanted.binary_search_by_key(&address, |&(a, _)| a) {
Ok(pos) => wanted[pos].1.push(index),
Err(pos) => wanted.insert(pos, (address, Vec::from([index]))),
}
}
for (_, indices) in &mut wanted {
indices.sort_unstable();
}
let mut collections: Vec<(u64, GlobalHeapIndex)> = Vec::new();
for element in &refs {
if element.length == 0
&& (is_undefined_addr(element.collection_address, offset_size)
|| element.collection_address == 0)
{
visitor("");
continue;
}
if is_undefined_addr(element.collection_address, offset_size) {
return Err(FormatError::VlDataError(
"non-empty VL element has an undefined heap address".into(),
));
}
let collection_address = element.collection_address.checked_add(base_address).ok_or(
FormatError::OffsetOverflow {
offset: element.collection_address,
length: base_address,
},
)?;
let collection_pos = match collections
.iter()
.position(|(address, _)| *address == collection_address)
{
Some(pos) => pos,
None => {
let keep = wanted
.binary_search_by_key(&collection_address, |&(a, _)| a)
.map(|pos| wanted[pos].1.as_slice())
.unwrap_or(&[]);
let collection = GlobalHeapIndex::parse_filtered(
source,
collection_address,
length_size,
|i| keep.binary_search(&i).is_ok(),
)?;
collections.push((collection_address, collection));
collections.len() - 1
}
};
let index = u16::try_from(element.object_index).map_err(|_| {
FormatError::VlDataError(format!(
"global heap object index {} does not fit u16",
element.object_index
))
})?;
let object = collections[collection_pos].1.get_object(index).ok_or(
FormatError::GlobalHeapObjectNotFound {
collection_address,
index,
},
)?;
if u64::from(element.length) > object.size {
return Err(FormatError::VlDataError(format!(
"VL element length {} exceeds global heap object size {}",
element.length, object.size
)));
}
let bytes = source.read_exact_at(object.data_address, element.length as usize)?;
let string = String::from_utf8_lossy(&bytes);
visitor(&string);
}
Ok(())
}
pub fn read_vl_strings_from_source<S: Source + ?Sized>(
source: &S,
raw_data: &[u8],
num_elements: u64,
offset_size: u8,
length_size: u8,
base_address: u64,
options: VlenStringReadOptions,
) -> Result<Vec<String>, FormatError> {
let mut strings = Vec::new();
visit_vl_strings_from_source(
source,
raw_data,
num_elements,
offset_size,
length_size,
base_address,
options,
|string| strings.push(String::from(string)),
)?;
Ok(strings)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum VlByteObject {
Null,
Bytes(Vec<u8>),
}
pub(crate) fn read_vl_byte_objects_from_source<S: Source + ?Sized>(
source: &S,
raw_data: &[u8],
num_elements: u64,
offset_size: u8,
length_size: u8,
base_address: u64,
element_size: usize,
options: VlenStringReadOptions,
) -> Result<Vec<VlByteObject>, FormatError> {
check_element_limit(num_elements, options)?;
let refs = parse_vl_references(raw_data, num_elements, offset_size)?;
payload_size(&refs, options)?;
let mut objects = Vec::with_capacity(refs.len());
let mut collections: Vec<(u64, GlobalHeapIndex)> = Vec::new();
for element in &refs {
if element.length == 0
&& (is_undefined_addr(element.collection_address, offset_size)
|| element.collection_address == 0)
{
objects.push(VlByteObject::Null);
continue;
}
if is_undefined_addr(element.collection_address, offset_size) {
return Err(FormatError::VlDataError(
"non-empty VL element has an undefined heap address".into(),
));
}
let collection_address = element.collection_address.checked_add(base_address).ok_or(
FormatError::OffsetOverflow {
offset: element.collection_address,
length: base_address,
},
)?;
let collection_pos = match collections
.iter()
.position(|(address, _)| *address == collection_address)
{
Some(pos) => pos,
None => {
let collection = GlobalHeapIndex::parse(source, collection_address, length_size)?;
collections.push((collection_address, collection));
collections.len() - 1
}
};
let index = u16::try_from(element.object_index).map_err(|_| {
FormatError::VlDataError(format!(
"global heap object index {} does not fit u16",
element.object_index
))
})?;
let object = collections[collection_pos].1.get_object(index).ok_or(
FormatError::GlobalHeapObjectNotFound {
collection_address,
index,
},
)?;
let byte_len = (element.length as u64)
.checked_mul(element_size as u64)
.ok_or(FormatError::OffsetOverflow {
offset: u64::from(element.length),
length: element_size as u64,
})?;
if byte_len > object.size {
return Err(FormatError::VlDataError(format!(
"VL element length {} ({} bytes) exceeds global heap object size {}",
element.length, byte_len, object.size
)));
}
let bytes = source.read_exact_at(object.data_address, byte_len.to_usize()?)?;
objects.push(VlByteObject::Bytes(bytes));
}
Ok(objects)
}
#[cfg(test)]
pub fn read_vl_strings(
file_data: &[u8],
raw_data: &[u8],
num_elements: u64,
offset_size: u8,
length_size: u8,
) -> Result<Vec<String>, FormatError> {
read_vl_strings_from_source(
&BytesSource::new(file_data),
raw_data,
num_elements,
offset_size,
length_size,
0,
VlenStringReadOptions::default(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn build_gcol_at(
file_data: &mut Vec<u8>,
offset: usize,
objects: &[(u16, &[u8])], ) {
let length_size = 8usize;
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; 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;
file_data[pos..pos + 4].copy_from_slice(b"GCOL");
file_data[pos + 4] = 1; 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()); 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;
}
file_data[pos..pos + 2].copy_from_slice(&0u16.to_le_bytes());
}
fn build_vl_refs(
strings: &[&str],
collection_address: u64,
start_index: u16,
offset_size: u8,
) -> Vec<u8> {
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"]);
}
#[cfg(feature = "std")]
#[test]
fn read_vl_strings_from_seekable_source() {
use std::io::Cursor;
use crate::source::ReadSeekSource;
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 source = ReadSeekSource::new(Cursor::new(file_data)).unwrap();
let strings = read_vl_strings_from_source(
&source,
&raw,
2,
8,
8,
0,
VlenStringReadOptions::default(),
)
.unwrap();
assert_eq!(strings, vec!["Alice", "Bob"]);
}
#[test]
fn null_vl_element_empty_string() {
let mut raw = Vec::new();
raw.extend_from_slice(&0u32.to_le_bytes()); raw.extend_from_slice(&u64::MAX.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 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 parse_vl_references_truncated_error() {
let raw = vec![0u8; 10]; let err = parse_vl_references(&raw, 1, 8).unwrap_err();
assert!(matches!(err, FormatError::UnexpectedEof { .. }));
}
}
#[cfg(test)]
mod embedded_slot_tests {
use super::*;
use crate::datatype::{CompoundMember, StringPadding};
fn vlen_string() -> Datatype {
Datatype::VariableLength {
is_string: true,
base_type: Box::new(Datatype::String {
size: 1,
charset: CharacterSet::Utf8,
padding: StringPadding::NullTerminate,
}),
padding: Some(StringPadding::NullTerminate),
charset: Some(CharacterSet::Utf8),
}
}
fn vlen_i32_sequence() -> Datatype {
Datatype::VariableLength {
is_string: false,
base_type: Box::new(Datatype::FixedPoint {
size: 4,
signed: true,
byte_order: crate::datatype::DatatypeByteOrder::LittleEndian,
bit_offset: 0,
bit_precision: 32,
}),
padding: None,
charset: None,
}
}
fn i32_type() -> Datatype {
Datatype::FixedPoint {
size: 4,
signed: true,
byte_order: crate::datatype::DatatypeByteOrder::LittleEndian,
bit_offset: 0,
bit_precision: 32,
}
}
fn member(name: &str, byte_offset: u64, datatype: Datatype) -> CompoundMember {
CompoundMember {
name: name.to_string(),
byte_offset,
datatype,
}
}
#[test]
fn a_plain_datatype_reaches_no_reference() {
assert_eq!(embedded_vlen_slots(&i32_type()), Some(Vec::new()));
}
#[test]
fn a_top_level_vlen_is_its_own_slot() {
assert_eq!(
embedded_vlen_slots(&vlen_string()),
Some(vec![EmbeddedVlSlot {
byte_offset: 0,
element_size: 1
}])
);
}
#[test]
fn compound_members_keep_their_own_element_size() {
let dt = Datatype::Compound {
size: 40,
members: vec![
member("label", 0, vlen_string()),
member("id", 16, i32_type()),
member("samples", 24, vlen_i32_sequence()),
],
};
assert_eq!(
embedded_vlen_slots(&dt),
Some(vec![
EmbeddedVlSlot {
byte_offset: 0,
element_size: 1
},
EmbeddedVlSlot {
byte_offset: 24,
element_size: 4
},
])
);
}
#[test]
fn nested_compounds_and_arrays_are_walked() {
let inner = Datatype::Compound {
size: 20,
members: vec![member("id", 0, i32_type()), member("s", 4, vlen_string())],
};
let nested = Datatype::Compound {
size: 24,
members: vec![
member("n", 0, i32_type()),
member("inner", 4, inner.clone()),
],
};
assert_eq!(
embedded_vlen_slots(&nested),
Some(vec![EmbeddedVlSlot {
byte_offset: 8,
element_size: 1
}])
);
let array = Datatype::Array {
base_type: Box::new(inner),
dimensions: vec![3],
};
assert_eq!(
embedded_vlen_slots(&array),
Some(vec![
EmbeddedVlSlot {
byte_offset: 4,
element_size: 1
},
EmbeddedVlSlot {
byte_offset: 24,
element_size: 1
},
EmbeddedVlSlot {
byte_offset: 44,
element_size: 1
},
])
);
}
#[test]
fn a_datatype_too_small_for_its_own_references_is_rejected() {
let dt = Datatype::Compound {
size: 8, members: vec![member("s", 0, vlen_string())],
};
assert_eq!(embedded_vlen_slots(&dt), None);
}
#[test]
fn an_array_declaring_absurd_dimensions_is_rejected_not_walked() {
let dt = Datatype::Array {
base_type: Box::new(vlen_string()),
dimensions: vec![u32::MAX, u32::MAX],
};
assert_eq!(embedded_vlen_slots(&dt), None);
}
#[test]
fn deeply_nested_arrays_cost_time_proportional_to_their_slots() {
let mut dt = vlen_string();
for _ in 0..17 {
dt = Datatype::Array {
base_type: Box::new(dt),
dimensions: vec![2],
};
}
let started = std::time::Instant::now();
let slots = embedded_vlen_slots(&dt).expect("a well-formed nesting must be walkable");
let elapsed = started.elapsed();
assert_eq!(slots.len(), 1 << 17, "one slot per leaf entry");
assert!(
elapsed < std::time::Duration::from_secs(10),
"walking {} slots took {elapsed:?}; the walk is no longer linear in its output",
slots.len()
);
}
#[test]
fn a_member_offset_beyond_the_address_space_is_rejected() {
let dt = Datatype::Compound {
size: 40,
members: vec![member("s", u64::MAX - 8, vlen_string())],
};
assert_eq!(embedded_vlen_slots(&dt), None);
}
}