#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, string::String, string::ToString, vec, vec::Vec};
use core::fmt;
use core::num::{NonZeroU32, NonZeroUsize};
use crate::attribute::AttributeMessage;
use crate::chunked_write::{ChunkMeta, ChunkOptions, ChunkProvider, FilterKind, StorageAllocation};
use crate::compound::CompoundType;
use crate::convert::TryToUsize;
use crate::dataspace::{Dataspace, DataspaceType};
use crate::datatype::{
CharacterSet, CompoundMember, Datatype, DatatypeByteOrder, EnumMember, StringPadding,
};
use crate::display::write_elided;
use crate::error::FormatError;
use crate::scaleoffset::{FillAvailability, ScaleOffset};
use crate::shared_message::DatatypeLocation;
pub fn make_f64_type() -> Datatype {
Datatype::FloatingPoint {
size: 8,
byte_order: DatatypeByteOrder::LittleEndian,
bit_offset: 0,
bit_precision: 64,
exponent_location: 52,
exponent_size: 11,
mantissa_location: 0,
mantissa_size: 52,
exponent_bias: 1023,
}
}
pub fn make_f32_type() -> Datatype {
Datatype::FloatingPoint {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
bit_offset: 0,
bit_precision: 32,
exponent_location: 23,
exponent_size: 8,
mantissa_location: 0,
mantissa_size: 23,
exponent_bias: 127,
}
}
pub fn make_i32_type() -> Datatype {
Datatype::FixedPoint {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 32,
}
}
pub fn make_i64_type() -> Datatype {
Datatype::FixedPoint {
size: 8,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 64,
}
}
pub fn make_u8_type() -> Datatype {
Datatype::FixedPoint {
size: 1,
byte_order: DatatypeByteOrder::LittleEndian,
signed: false,
bit_offset: 0,
bit_precision: 8,
}
}
pub fn make_i8_type() -> Datatype {
Datatype::FixedPoint {
size: 1,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 8,
}
}
pub fn make_i16_type() -> Datatype {
Datatype::FixedPoint {
size: 2,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 16,
}
}
pub fn make_u16_type() -> Datatype {
Datatype::FixedPoint {
size: 2,
byte_order: DatatypeByteOrder::LittleEndian,
signed: false,
bit_offset: 0,
bit_precision: 16,
}
}
pub fn make_u32_type() -> Datatype {
Datatype::FixedPoint {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
signed: false,
bit_offset: 0,
bit_precision: 32,
}
}
pub fn make_u64_type() -> Datatype {
Datatype::FixedPoint {
size: 8,
byte_order: DatatypeByteOrder::LittleEndian,
signed: false,
bit_offset: 0,
bit_precision: 64,
}
}
pub fn make_object_reference_type() -> Datatype {
Datatype::Reference {
size: 8,
ref_type: crate::datatype::ReferenceType::Object,
}
}
pub fn make_vlen_string_type(charset: CharacterSet) -> Datatype {
Datatype::VariableLength {
is_string: true,
padding: Some(StringPadding::NullTerminate),
charset: Some(charset),
base_type: Box::new(make_u8_type()),
}
}
pub struct CompoundTypeBuilder {
fields: Vec<(String, Datatype)>,
}
impl CompoundTypeBuilder {
pub fn new() -> Self {
Self { fields: Vec::new() }
}
pub fn field(mut self, name: &str, datatype: Datatype) -> Self {
self.fields.push((name.to_string(), datatype));
self
}
pub fn f64_field(self, name: &str) -> Self {
self.field(name, make_f64_type())
}
pub fn f32_field(self, name: &str) -> Self {
self.field(name, make_f32_type())
}
pub fn i32_field(self, name: &str) -> Self {
self.field(name, make_i32_type())
}
pub fn i64_field(self, name: &str) -> Self {
self.field(name, make_i64_type())
}
pub fn u8_field(self, name: &str) -> Self {
self.field(name, make_u8_type())
}
pub fn i8_field(self, name: &str) -> Self {
self.field(name, make_i8_type())
}
pub fn i16_field(self, name: &str) -> Self {
self.field(name, make_i16_type())
}
pub fn u16_field(self, name: &str) -> Self {
self.field(name, make_u16_type())
}
pub fn u32_field(self, name: &str) -> Self {
self.field(name, make_u32_type())
}
pub fn u64_field(self, name: &str) -> Self {
self.field(name, make_u64_type())
}
pub fn build(self) -> Result<Datatype, FormatError> {
if self.fields.is_empty() {
return Err(FormatError::EmptyCompoundType);
}
let mut offset = 0u64;
let mut members = Vec::with_capacity(self.fields.len());
for (name, dt) in self.fields {
let sz = dt.type_size();
members.push(CompoundMember {
name,
byte_offset: offset,
datatype: dt,
});
offset += sz as u64;
}
if offset == 0 {
return Err(FormatError::InvalidCompoundSize);
}
Ok(Datatype::Compound {
#[expect(
clippy::cast_possible_truncation,
reason = "accumulated compound size is stored in the 4-byte datatype size field"
)]
size: offset as u32,
members,
})
}
}
impl Default for CompoundTypeBuilder {
fn default() -> Self {
Self::new()
}
}
mod complex_component {
pub trait Sealed {}
}
pub(crate) trait ComplexComponent: complex_component::Sealed + Copy {
fn datatype() -> Datatype;
fn encode_le_into(self, dst: &mut [u8]);
#[cfg(feature = "serde")]
fn decode_le(bytes: &[u8]) -> Self;
}
macro_rules! impl_complex_component {
($($ty:ty => $make:ident),* $(,)?) => {
$(
impl complex_component::Sealed for $ty {}
impl ComplexComponent for $ty {
fn datatype() -> Datatype {
$make()
}
fn encode_le_into(self, dst: &mut [u8]) {
dst.copy_from_slice(&self.to_le_bytes());
}
#[cfg(feature = "serde")]
fn decode_le(bytes: &[u8]) -> Self {
Self::from_le_bytes(
bytes
.try_into()
.expect("caller slices exactly one component"),
)
}
}
)*
};
}
impl_complex_component! {
f64 => make_f64_type,
f32 => make_f32_type,
i64 => make_i64_type,
i32 => make_i32_type,
i16 => make_i16_type,
i8 => make_i8_type,
u64 => make_u64_type,
u32 => make_u32_type,
u16 => make_u16_type,
u8 => make_u8_type,
}
pub struct ExplicitCompoundTypeBuilder {
size: u32,
fields: Vec<CompoundMember>,
}
impl ExplicitCompoundTypeBuilder {
pub fn field(mut self, name: &str, byte_offset: u64, datatype: Datatype) -> Self {
self.fields.push(CompoundMember {
name: name.to_string(),
byte_offset,
datatype,
});
self
}
pub fn f64_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_f64_type())
}
pub fn f32_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_f32_type())
}
pub fn i32_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_i32_type())
}
pub fn i64_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_i64_type())
}
pub fn u8_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_u8_type())
}
pub fn i8_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_i8_type())
}
pub fn i16_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_i16_type())
}
pub fn u16_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_u16_type())
}
pub fn u32_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_u32_type())
}
pub fn u64_field(self, name: &str, byte_offset: u64) -> Self {
self.field(name, byte_offset, make_u64_type())
}
pub fn build(mut self) -> Result<Datatype, crate::error::FormatError> {
use crate::error::FormatError;
if self.size == 0 {
return Err(FormatError::InvalidCompoundSize);
}
if self.fields.is_empty() {
return Err(FormatError::EmptyCompoundType);
}
for (index, field) in self.fields.iter().enumerate() {
if self.fields[..index]
.iter()
.any(|earlier| earlier.name == field.name)
{
return Err(FormatError::DuplicateCompoundField(field.name.clone()));
}
let field_size = field.datatype.type_size();
let end = field.byte_offset.checked_add(u64::from(field_size));
if field_size == 0 || end.is_none_or(|end| end > u64::from(self.size)) {
return Err(FormatError::CompoundFieldOutOfBounds {
name: field.name.clone(),
offset: field.byte_offset,
field_size,
compound_size: self.size,
});
}
}
self.fields.sort_by_key(|field| field.byte_offset);
for fields in self.fields.windows(2) {
let first_end = fields[0].byte_offset + u64::from(fields[0].datatype.type_size());
if first_end > fields[1].byte_offset {
return Err(FormatError::CompoundFieldOverlap {
first: fields[0].name.clone(),
second: fields[1].name.clone(),
});
}
}
Ok(Datatype::Compound {
size: self.size,
members: self.fields,
})
}
}
impl CompoundTypeBuilder {
pub fn with_size(size: u32) -> ExplicitCompoundTypeBuilder {
ExplicitCompoundTypeBuilder {
size,
fields: Vec::new(),
}
}
}
pub struct EnumTypeBuilder {
base_type: Datatype,
members: Vec<(String, PendingEnumValue)>,
}
enum PendingEnumValue {
Int(i64),
Raw(Vec<u8>),
}
impl EnumTypeBuilder {
pub fn i32_based() -> Self {
Self::with_base(make_i32_type())
}
pub fn u8_based() -> Self {
Self::with_base(make_u8_type())
}
pub fn with_base(base_type: Datatype) -> Self {
Self {
base_type,
members: Vec::new(),
}
}
pub fn value(mut self, name: &str, val: i32) -> Self {
self.members
.push((name.to_string(), PendingEnumValue::Int(val as i64)));
self
}
pub fn u8_value(self, name: &str, val: u8) -> Self {
self.value(name, i32::from(val))
}
pub fn i64_value(mut self, name: &str, val: i64) -> Self {
self.members
.push((name.to_string(), PendingEnumValue::Int(val)));
self
}
pub fn raw_value(mut self, name: &str, bytes: &[u8]) -> Self {
self.members
.push((name.to_string(), PendingEnumValue::Raw(bytes.to_vec())));
self
}
pub fn build(self) -> Result<Datatype, FormatError> {
let size = self.base_type.type_size();
let signed = match &self.base_type {
Datatype::FixedPoint { signed, .. } => *signed,
_ => return Err(FormatError::EnumBaseNotInteger),
};
let width = size.to_usize()?;
let mut members = Vec::with_capacity(self.members.len());
for (name, pending) in self.members {
let value = match pending {
PendingEnumValue::Raw(bytes) => {
if bytes.len() != width {
return Err(FormatError::EnumMemberValueSize(name, size, bytes.len()));
}
bytes
}
PendingEnumValue::Int(v) => {
if !int_fits(v, width, signed) {
return Err(FormatError::EnumMemberValueRange(name, v, size));
}
v.to_le_bytes()[..width].to_vec()
}
};
members.push(EnumMember { name, value });
}
Ok(Datatype::Enumeration {
size,
base_type: Box::new(self.base_type),
members,
})
}
}
fn int_fits(v: i64, width: usize, signed: bool) -> bool {
if width == 0 {
return false;
}
if width >= 8 {
return signed || v >= 0;
}
let bits = width * 8;
if signed {
let min = -(1i64 << (bits - 1));
let max = (1i64 << (bits - 1)) - 1;
(min..=max).contains(&v)
} else {
let max = (1i64 << bits) - 1;
(0..=max).contains(&v)
}
}
pub(crate) enum AttrSpec {
Value(AttrValue),
Verbatim(AttributeMessage),
VerbatimVarLen {
message: AttributeMessage,
strings: Vec<String>,
},
}
impl AttrSpec {
pub(crate) fn to_message(&self, name: &str) -> AttributeMessage {
match self {
Self::Value(v) => build_attr_message(name, v),
Self::Verbatim(m) | Self::VerbatimVarLen { message: m, .. } => m.clone(),
}
}
pub(crate) fn var_len_strings(&self) -> Option<&[String]> {
match self {
Self::Value(AttrValue::VarLenAsciiArray(strings))
| Self::VerbatimVarLen { strings, .. } => Some(strings),
_ => None,
}
}
}
fn numeric_scalar_attr(name: &str, datatype: Datatype, raw_data: &[u8]) -> AttributeMessage {
AttributeMessage {
name: name.to_string(),
datatype,
dataspace: scalar_ds(),
raw_data: raw_data.to_vec(),
datatype_location: DatatypeLocation::Inline,
}
}
fn numeric_array_attr<T: Copy, const N: usize>(
name: &str,
datatype: Datatype,
values: &[T],
to_le_bytes: fn(T) -> [u8; N],
) -> AttributeMessage {
let mut raw_data = Vec::with_capacity(values.len() * N);
for &v in values {
raw_data.extend_from_slice(&to_le_bytes(v));
}
AttributeMessage {
name: name.to_string(),
datatype,
dataspace: simple_1d(values.len() as u64),
raw_data,
datatype_location: DatatypeLocation::Inline,
}
}
fn fixed_string_attr<S: AsRef<str>>(
name: &str,
values: &[S],
width: Option<NonZeroU32>,
charset: CharacterSet,
dataspace: Dataspace,
) -> AttributeMessage {
let (raw_data, width) = pad_fixed_strings(values, width.unwrap_or(NonZeroU32::MIN));
AttributeMessage {
name: name.to_string(),
datatype: Datatype::String {
size: width.get(),
padding: StringPadding::NullPad,
charset,
},
dataspace,
raw_data,
datatype_location: DatatypeLocation::Inline,
}
}
fn fixed_string_scalar_attr(
name: &str,
value: &str,
width: Option<NonZeroU32>,
charset: CharacterSet,
) -> AttributeMessage {
fixed_string_attr(
name,
core::slice::from_ref(&value),
width,
charset,
scalar_ds(),
)
}
fn fixed_string_array_attr<S: AsRef<str>>(
name: &str,
values: &[S],
width: Option<NonZeroU32>,
charset: CharacterSet,
) -> AttributeMessage {
let dataspace = simple_1d(values.len() as u64);
fixed_string_attr(name, values, width, charset, dataspace)
}
pub(crate) fn build_attr_message(name: &str, value: &AttrValue) -> AttributeMessage {
match value {
AttrValue::F32(v) => numeric_scalar_attr(name, make_f32_type(), &v.to_le_bytes()),
AttrValue::F32Array(a) => numeric_array_attr(name, make_f32_type(), a, f32::to_le_bytes),
AttrValue::F64(v) => numeric_scalar_attr(name, make_f64_type(), &v.to_le_bytes()),
AttrValue::F64Array(a) => numeric_array_attr(name, make_f64_type(), a, f64::to_le_bytes),
AttrValue::I8(v) => numeric_scalar_attr(name, make_i8_type(), &v.to_le_bytes()),
AttrValue::I8Array(a) => numeric_array_attr(name, make_i8_type(), a, i8::to_le_bytes),
AttrValue::I16(v) => numeric_scalar_attr(name, make_i16_type(), &v.to_le_bytes()),
AttrValue::I16Array(a) => numeric_array_attr(name, make_i16_type(), a, i16::to_le_bytes),
AttrValue::I32(v) => numeric_scalar_attr(name, make_i32_type(), &v.to_le_bytes()),
AttrValue::I32Array(a) => numeric_array_attr(name, make_i32_type(), a, i32::to_le_bytes),
AttrValue::I64(v) => numeric_scalar_attr(name, make_i64_type(), &v.to_le_bytes()),
AttrValue::I64Array(a) => numeric_array_attr(name, make_i64_type(), a, i64::to_le_bytes),
AttrValue::U8(v) => numeric_scalar_attr(name, make_u8_type(), &v.to_le_bytes()),
AttrValue::U8Array(a) => numeric_array_attr(name, make_u8_type(), a, u8::to_le_bytes),
AttrValue::U16(v) => numeric_scalar_attr(name, make_u16_type(), &v.to_le_bytes()),
AttrValue::U16Array(a) => numeric_array_attr(name, make_u16_type(), a, u16::to_le_bytes),
AttrValue::U32(v) => numeric_scalar_attr(name, make_u32_type(), &v.to_le_bytes()),
AttrValue::U32Array(a) => numeric_array_attr(name, make_u32_type(), a, u32::to_le_bytes),
AttrValue::U64(v) => numeric_scalar_attr(name, make_u64_type(), &v.to_le_bytes()),
AttrValue::U64Array(a) => numeric_array_attr(name, make_u64_type(), a, u64::to_le_bytes),
AttrValue::String(s) => fixed_string_scalar_attr(name, s, None, CharacterSet::Utf8),
AttrValue::StringSized { value, width } => {
fixed_string_scalar_attr(name, value, Some(*width), CharacterSet::Utf8)
}
AttrValue::StringArray(arr) => fixed_string_array_attr(name, arr, None, CharacterSet::Utf8),
AttrValue::StringArraySized { values, width } => {
fixed_string_array_attr(name, values, Some(*width), CharacterSet::Utf8)
}
AttrValue::AsciiString(s) => fixed_string_scalar_attr(name, s, None, CharacterSet::Ascii),
AttrValue::AsciiStringSized { value, width } => {
fixed_string_scalar_attr(name, value, Some(*width), CharacterSet::Ascii)
}
AttrValue::AsciiStringArray(arr) => {
fixed_string_array_attr(name, arr, None, CharacterSet::Ascii)
}
AttrValue::AsciiStringArraySized { values, width } => {
fixed_string_array_attr(name, values, Some(*width), CharacterSet::Ascii)
}
AttrValue::VarLenAsciiArray(strings) => {
AttributeMessage {
name: name.to_string(),
raw_data: vl_string_reference_bytes(strings),
datatype: Datatype::VariableLength {
is_string: false,
padding: None,
charset: None,
base_type: Box::new(Datatype::String {
size: 1,
padding: StringPadding::NullTerminate,
charset: CharacterSet::Ascii,
}),
},
dataspace: simple_1d(strings.len() as u64),
datatype_location: DatatypeLocation::Inline,
}
}
}
}
pub(crate) fn vl_string_reference_bytes(strings: &[String]) -> Vec<u8> {
let mut raw = Vec::with_capacity(strings.len() * VL_REF_SIZE);
for (i, s) in strings.iter().enumerate() {
#[expect(
clippy::cast_possible_truncation,
reason = "VLEN string length is written into the 4-byte length prefix of the variable-length reference"
)]
raw.extend_from_slice(&(s.len() as u32).to_le_bytes());
raw.extend_from_slice(&0u64.to_le_bytes()); #[expect(
clippy::cast_possible_truncation,
reason = "1-based heap object index is written into the 4-byte object-index field of the variable-length reference"
)]
raw.extend_from_slice(&((i % MAX_HEAP_OBJECTS + 1) as u32).to_le_bytes());
}
raw
}
pub(crate) const MAX_HEAP_OBJECTS: usize = u16::MAX as usize;
pub(crate) fn build_global_heap_collections(strings: &[&str]) -> Vec<Vec<u8>> {
let objects: Vec<&[u8]> = strings.iter().map(|s| s.as_bytes()).collect();
build_global_heap_collections_from_bytes(&objects)
}
pub(crate) fn build_global_heap_collections_from_bytes(objects: &[&[u8]]) -> Vec<Vec<u8>> {
objects
.chunks(MAX_HEAP_OBJECTS)
.map(build_global_heap_collection_bytes)
.collect()
}
fn build_global_heap_collection_bytes(objects: &[&[u8]]) -> Vec<u8> {
debug_assert!(
objects.len() <= MAX_HEAP_OBJECTS,
"a collection's 2-byte object index cannot address more than {MAX_HEAP_OBJECTS} objects"
);
let length_size = 8usize;
let header_size = 8 + length_size;
let mut obj_size_total = 0usize;
for obj in objects {
let obj_header = 8 + length_size; let padded_data_len = (obj.len() + 7) & !7; obj_size_total += obj_header + padded_data_len;
}
obj_size_total += 8 + length_size; let collection_size = header_size + obj_size_total;
let min_collection_size = 4096;
let padded_collection = ((collection_size.max(min_collection_size)) + 7) & !7;
let mut buf = Vec::with_capacity(padded_collection);
buf.extend_from_slice(b"GCOL");
buf.push(1); buf.extend_from_slice(&[0u8; 3]); buf.extend_from_slice(&(padded_collection as u64).to_le_bytes());
for (i, obj) in objects.iter().enumerate() {
#[expect(
clippy::cast_possible_truncation,
reason = "1-based heap object index is written into the 2-byte heap-object index field"
)]
let index = (i + 1) as u16;
buf.extend_from_slice(&index.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&(obj.len() as u64).to_le_bytes());
buf.extend_from_slice(obj);
let padded = (obj.len() + 7) & !7;
for _ in obj.len()..padded {
buf.push(0);
}
}
let free_total_size = padded_collection - buf.len();
buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&(free_total_size as u64).to_le_bytes());
buf.resize(padded_collection, 0);
buf
}
pub(crate) fn patch_vl_refs(raw_data: &mut [u8], collection_addresses: &[u64]) {
let count = raw_data.len() / VL_REF_SIZE;
for i in 0..count {
let address = collection_addresses[i / MAX_HEAP_OBJECTS];
let addr_offset = i * VL_REF_SIZE + 4; raw_data[addr_offset..addr_offset + 8].copy_from_slice(&address.to_le_bytes());
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum VlStringElement {
Null,
Bytes(Vec<u8>),
}
pub(crate) const VL_REF_SIZE: usize = 16;
#[derive(Clone)]
pub(crate) struct VlStringStaging {
pub collections: Vec<Vec<u8>>,
pub patch_offsets: Vec<usize>,
}
pub(crate) fn stage_vl_elements(
elements: &[VlStringElement],
element_size: NonZeroUsize,
) -> (Vec<u8>, VlStringStaging) {
stage_vl_payloads(
elements.iter().map(|e| match e {
VlStringElement::Null => None,
VlStringElement::Bytes(bytes) => Some(bytes.as_slice()),
}),
element_size,
)
}
pub(crate) fn stage_vl_payloads<'a>(
payloads: impl ExactSizeIterator<Item = Option<&'a [u8]>>,
element_size: NonZeroUsize,
) -> (Vec<u8>, VlStringStaging) {
let count = payloads.len();
let mut objects: Vec<&[u8]> = Vec::new();
let mut refs = Vec::with_capacity(count * VL_REF_SIZE);
let mut patch_offsets = Vec::with_capacity(count);
for element in payloads {
match element {
None => {
refs.extend_from_slice(&0u32.to_le_bytes()); refs.extend_from_slice(&0u64.to_le_bytes()); refs.extend_from_slice(&0u32.to_le_bytes()); }
Some(bytes) => {
patch_offsets.push(refs.len());
#[expect(
clippy::cast_possible_truncation,
reason = "VL element length (element count) is written into the 4-byte \
length prefix of the variable-length reference"
)]
refs.extend_from_slice(&((bytes.len() / element_size) as u32).to_le_bytes());
refs.extend_from_slice(&0u64.to_le_bytes()); let index = objects.len() % MAX_HEAP_OBJECTS + 1;
#[expect(
clippy::cast_possible_truncation,
reason = "1-based heap object index is written into the 4-byte object-index \
field of the variable-length reference"
)]
refs.extend_from_slice(&(index as u32).to_le_bytes());
objects.push(bytes);
}
}
}
(
refs,
VlStringStaging {
collections: build_global_heap_collections_from_bytes(&objects),
patch_offsets,
},
)
}
pub(crate) fn stage_embedded_vl_elements(
mut raw: Vec<u8>,
offsets: &[usize],
elements: &[VlStringElement],
) -> (Vec<u8>, VlStringStaging) {
debug_assert_eq!(
offsets.len(),
elements.len(),
"one staged payload per embedded variable-length reference"
);
let mut objects: Vec<&[u8]> = Vec::new();
let mut patch_offsets = Vec::with_capacity(elements.len());
for (&offset, element) in offsets.iter().zip(elements) {
let slot = &mut raw[offset..offset + VL_REF_SIZE];
match element {
VlStringElement::Null => slot.fill(0),
VlStringElement::Bytes(bytes) => {
patch_offsets.push(offset);
slot[4..12].fill(0);
let index = objects.len() % MAX_HEAP_OBJECTS + 1;
#[expect(
clippy::cast_possible_truncation,
reason = "1-based heap object index is written into the 4-byte object-index \
field of the variable-length reference"
)]
slot[12..16].copy_from_slice(&(index as u32).to_le_bytes());
objects.push(bytes);
}
}
}
(
raw,
VlStringStaging {
collections: build_global_heap_collections_from_bytes(&objects),
patch_offsets,
},
)
}
pub(crate) fn patch_vl_refs_masked(
raw_data: &mut [u8],
patch_offsets: &[usize],
collection_addresses: &[u64],
) {
for (object_ordinal, &offset) in patch_offsets.iter().enumerate() {
let address = collection_addresses[object_ordinal / MAX_HEAP_OBJECTS];
let addr_offset = offset + 4; raw_data[addr_offset..addr_offset + 8].copy_from_slice(&address.to_le_bytes());
}
}
fn fixed_string_size(len: usize) -> NonZeroU32 {
NonZeroU32::new(u32::try_from(len).unwrap_or(u32::MAX)).unwrap_or(NonZeroU32::MIN)
}
fn derived_string_width<S: AsRef<str>>(values: &[S]) -> NonZeroU32 {
fixed_string_size(values.iter().map(|s| s.as_ref().len()).max().unwrap_or(0))
}
fn check_fixed_width<S: AsRef<str>>(values: &[S], width: NonZeroU32) -> Result<(), FormatError> {
for (index, value) in values.iter().enumerate() {
let len = value.as_ref().len();
if len > width.get() as usize {
return Err(FormatError::FixedStringTooLong {
index,
len,
width: width.get(),
});
}
}
Ok(())
}
fn pad_fixed_strings<S: AsRef<str>>(values: &[S], min_width: NonZeroU32) -> (Vec<u8>, NonZeroU32) {
let width = min_width.max(derived_string_width(values));
let width_bytes = width.get() as usize;
let mut raw = Vec::with_capacity(values.len().saturating_mul(width_bytes));
for value in values {
let bytes = value.as_ref().as_bytes();
let bytes = &bytes[..bytes.len().min(width_bytes)];
raw.extend_from_slice(bytes);
raw.resize(raw.len() + (width_bytes - bytes.len()), 0);
}
(raw, width)
}
fn encode_fixed_strings<S: AsRef<str>>(
values: &[S],
width: NonZeroU32,
) -> Result<Vec<u8>, FormatError> {
check_fixed_width(values, width)?;
Ok(pad_fixed_strings(values, width).0)
}
pub(crate) fn scalar_ds() -> Dataspace {
Dataspace {
space_type: DataspaceType::Scalar,
rank: 0,
dimensions: vec![],
max_dimensions: None,
}
}
pub(crate) fn simple_1d(n: u64) -> Dataspace {
Dataspace {
space_type: DataspaceType::Simple,
rank: 1,
dimensions: vec![n],
max_dimensions: None,
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum AttrValue {
F32(f32),
F32Array(Vec<f32>),
F64(f64),
F64Array(Vec<f64>),
I8(i8),
I8Array(Vec<i8>),
I16(i16),
I16Array(Vec<i16>),
I32(i32),
I32Array(Vec<i32>),
I64(i64),
I64Array(Vec<i64>),
U8(u8),
U8Array(Vec<u8>),
U16(u16),
U16Array(Vec<u16>),
U32(u32),
U32Array(Vec<u32>),
U64(u64),
U64Array(Vec<u64>),
String(String),
#[non_exhaustive]
StringSized {
value: String,
width: NonZeroU32,
},
StringArray(Vec<String>),
#[non_exhaustive]
StringArraySized {
values: Vec<String>,
width: NonZeroU32,
},
AsciiString(String),
#[non_exhaustive]
AsciiStringSized {
value: String,
width: NonZeroU32,
},
AsciiStringArray(Vec<String>),
#[non_exhaustive]
AsciiStringArraySized {
values: Vec<String>,
width: NonZeroU32,
},
VarLenAsciiArray(Vec<String>),
}
impl AttrValue {
pub fn ascii_string_sized(value: impl Into<String>, width: u32) -> Result<Self, FormatError> {
let value = value.into();
let width = checked_width(core::slice::from_ref(&value), width)?;
Ok(Self::AsciiStringSized { value, width })
}
pub fn ascii_string_array_sized(values: Vec<String>, width: u32) -> Result<Self, FormatError> {
let width = checked_width(&values, width)?;
Ok(Self::AsciiStringArraySized { values, width })
}
pub fn string_sized(value: impl Into<String>, width: u32) -> Result<Self, FormatError> {
let value = value.into();
let width = checked_width(core::slice::from_ref(&value), width)?;
Ok(Self::StringSized { value, width })
}
pub fn string_array_sized(values: Vec<String>, width: u32) -> Result<Self, FormatError> {
let width = checked_width(&values, width)?;
Ok(Self::StringArraySized { values, width })
}
}
fn checked_width<S: AsRef<str>>(values: &[S], width: u32) -> Result<NonZeroU32, FormatError> {
let width = NonZeroU32::new(width).ok_or(FormatError::ZeroFixedStringWidth)?;
check_fixed_width(values, width)?;
Ok(width)
}
fn declared_width<S: AsRef<str>>(values: &[S], stored: u32) -> Option<NonZeroU32> {
let stored = NonZeroU32::new(stored)?;
(stored > derived_string_width(values)).then_some(stored)
}
pub(crate) fn decoded_fixed_string(value: String, width: u32, charset: &CharacterSet) -> AttrValue {
let ascii = *charset == CharacterSet::Ascii;
match (declared_width(core::slice::from_ref(&value), width), ascii) {
(Some(width), true) => AttrValue::AsciiStringSized { value, width },
(Some(width), false) => AttrValue::StringSized { value, width },
(None, true) => AttrValue::AsciiString(value),
(None, false) => AttrValue::String(value),
}
}
pub(crate) fn decoded_fixed_string_array(
values: Vec<String>,
width: u32,
charset: &CharacterSet,
) -> AttrValue {
let ascii = *charset == CharacterSet::Ascii;
match (declared_width(&values, width), ascii) {
(Some(width), true) => AttrValue::AsciiStringArraySized { values, width },
(Some(width), false) => AttrValue::StringArraySized { values, width },
(None, true) => AttrValue::AsciiStringArray(values),
(None, false) => AttrValue::StringArray(values),
}
}
impl AttrValue {
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s)
| Self::AsciiString(s)
| Self::StringSized { value: s, .. }
| Self::AsciiStringSized { value: s, .. } => Some(s),
Self::StringArray(v)
| Self::AsciiStringArray(v)
| Self::VarLenAsciiArray(v)
| Self::StringArraySized { values: v, .. }
| Self::AsciiStringArraySized { values: v, .. }
if v.len() == 1 =>
{
Some(&v[0])
}
_ => None,
}
}
pub fn as_strings(&self) -> Option<&[String]> {
match self {
Self::String(s)
| Self::AsciiString(s)
| Self::StringSized { value: s, .. }
| Self::AsciiStringSized { value: s, .. } => Some(core::slice::from_ref(s)),
Self::StringArray(v)
| Self::AsciiStringArray(v)
| Self::VarLenAsciiArray(v)
| Self::StringArraySized { values: v, .. }
| Self::AsciiStringArraySized { values: v, .. } => Some(v),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
self.single_int()
}
pub fn as_u64(&self) -> Option<u64> {
self.single_int()
}
pub fn to_i64s(&self) -> Option<Vec<i64>> {
self.int_elements()
}
pub fn to_u64s(&self) -> Option<Vec<u64>> {
self.int_elements()
}
fn single_int<T: TryFrom<i128>>(&self) -> Option<T> {
let one: i128 = match self {
Self::I8(v) => (*v).into(),
Self::I16(v) => (*v).into(),
Self::I32(v) => (*v).into(),
Self::I64(v) => (*v).into(),
Self::U8(v) => (*v).into(),
Self::U16(v) => (*v).into(),
Self::U32(v) => (*v).into(),
Self::U64(v) => (*v).into(),
Self::I8Array(v) if v.len() == 1 => v[0].into(),
Self::I16Array(v) if v.len() == 1 => v[0].into(),
Self::I32Array(v) if v.len() == 1 => v[0].into(),
Self::I64Array(v) if v.len() == 1 => v[0].into(),
Self::U8Array(v) if v.len() == 1 => v[0].into(),
Self::U16Array(v) if v.len() == 1 => v[0].into(),
Self::U32Array(v) if v.len() == 1 => v[0].into(),
Self::U64Array(v) if v.len() == 1 => v[0].into(),
_ => return None,
};
T::try_from(one).ok()
}
fn int_elements<T: TryFrom<i128>>(&self) -> Option<Vec<T>> {
match self {
Self::I8Array(v) => many_ints(v),
Self::I16Array(v) => many_ints(v),
Self::I32Array(v) => many_ints(v),
Self::I64Array(v) => many_ints(v),
Self::U8Array(v) => many_ints(v),
Self::U16Array(v) => many_ints(v),
Self::U32Array(v) => many_ints(v),
Self::U64Array(v) => many_ints(v),
_ => Some(vec![self.single_int()?]),
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Self::F32(v) => Some(f64::from(*v)),
Self::F64(v) => Some(*v),
Self::F32Array(v) if v.len() == 1 => Some(f64::from(v[0])),
Self::F64Array(v) if v.len() == 1 => Some(v[0]),
_ => None,
}
}
pub fn to_f64s(&self) -> Option<Vec<f64>> {
match self {
Self::F32(v) => Some(vec![f64::from(*v)]),
Self::F32Array(v) => Some(v.iter().copied().map(f64::from).collect()),
Self::F64(v) => Some(vec![*v]),
Self::F64Array(v) => Some(v.clone()),
_ => None,
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::F32(_) => "f32",
Self::F32Array(_) => "f32[]",
Self::F64(_) => "f64",
Self::F64Array(_) => "f64[]",
Self::I8(_) => "i8",
Self::I8Array(_) => "i8[]",
Self::I16(_) => "i16",
Self::I16Array(_) => "i16[]",
Self::I32(_) => "i32",
Self::I32Array(_) => "i32[]",
Self::I64(_) => "i64",
Self::I64Array(_) => "i64[]",
Self::U8(_) => "u8",
Self::U8Array(_) => "u8[]",
Self::U16(_) => "u16",
Self::U16Array(_) => "u16[]",
Self::U32(_) => "u32",
Self::U32Array(_) => "u32[]",
Self::U64(_) => "u64",
Self::U64Array(_) => "u64[]",
Self::String(_) => "string",
Self::StringSized { .. } => "sized_string",
Self::StringArray(_) => "string[]",
Self::StringArraySized { .. } => "sized_string[]",
Self::AsciiString(_) => "ascii_string",
Self::AsciiStringSized { .. } => "sized_ascii_string",
Self::AsciiStringArray(_) => "ascii_string[]",
Self::AsciiStringArraySized { .. } => "sized_ascii_string[]",
Self::VarLenAsciiArray(_) => "vlen_ascii_string[]",
}
}
}
fn many_ints<T: TryFrom<i128>, E: Into<i128> + Copy>(values: &[E]) -> Option<Vec<T>> {
values.iter().map(|&e| T::try_from(e.into()).ok()).collect()
}
const ATTR_DISPLAY_MAX_ELEMENTS: usize = 8;
impl fmt::Display for AttrValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::F32(v) => write!(f, "{v:?}"),
Self::F64(v) => write!(f, "{v:?}"),
Self::I8(v) => write!(f, "{v}"),
Self::I16(v) => write!(f, "{v}"),
Self::I32(v) => write!(f, "{v}"),
Self::I64(v) => write!(f, "{v}"),
Self::U8(v) => write!(f, "{v}"),
Self::U16(v) => write!(f, "{v}"),
Self::U32(v) => write!(f, "{v}"),
Self::U64(v) => write!(f, "{v}"),
Self::String(v)
| Self::AsciiString(v)
| Self::StringSized { value: v, .. }
| Self::AsciiStringSized { value: v, .. } => write!(f, "{v:?}"),
Self::F32Array(v) => write_elements(f, v),
Self::F64Array(v) => write_elements(f, v),
Self::I8Array(v) => write_elements(f, v),
Self::I16Array(v) => write_elements(f, v),
Self::I32Array(v) => write_elements(f, v),
Self::I64Array(v) => write_elements(f, v),
Self::U8Array(v) => write_elements(f, v),
Self::U16Array(v) => write_elements(f, v),
Self::U32Array(v) => write_elements(f, v),
Self::U64Array(v) => write_elements(f, v),
Self::StringArray(v)
| Self::AsciiStringArray(v)
| Self::VarLenAsciiArray(v)
| Self::StringArraySized { values: v, .. }
| Self::AsciiStringArraySized { values: v, .. } => write_elements(f, v),
}
}
}
fn write_elements<T: fmt::Debug>(f: &mut fmt::Formatter<'_>, values: &[T]) -> fmt::Result {
f.write_str("[")?;
for (i, value) in values.iter().take(ATTR_DISPLAY_MAX_ELEMENTS).enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{value:?}")?;
}
write_elided(f, values.len().saturating_sub(ATTR_DISPLAY_MAX_ELEMENTS))?;
f.write_str("]")
}
#[cfg(feature = "provenance")]
#[derive(Debug, Clone)]
pub struct ProvenanceConfig {
pub creator: String,
pub timestamp: String,
pub source: Option<String>,
}
pub(crate) struct RawChunkPayload {
pub(crate) chunk_dims: Vec<u64>,
pub(crate) element_size: NonZeroUsize,
pub(crate) pipeline_message: Option<Vec<u8>>,
pub(crate) meta: Vec<ChunkMeta>,
pub(crate) provider: core::panic::AssertUnwindSafe<Box<dyn ChunkProvider>>,
}
pub(crate) struct ProducedPayload {
pub(crate) total_bytes: u64,
pub(crate) block_bytes: u64,
pub(crate) provider: core::panic::AssertUnwindSafe<Box<dyn ChunkProvider>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ObjectRefTarget {
Path(String),
Raw(u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ObjectRefPatch {
pub byte_offset: usize,
pub target: ObjectRefTarget,
}
pub(crate) fn write_reference_address(raw: &mut [u8], byte_offset: usize, address: u64) {
debug_assert!(
byte_offset + 8 <= raw.len(),
"object-reference slot at {byte_offset} does not fit {} element bytes",
raw.len()
);
if let Some(slot) = raw.get_mut(byte_offset..byte_offset + 8) {
slot.copy_from_slice(&address.to_le_bytes());
}
}
pub struct DatasetBuilder {
pub(crate) name: String,
pub(crate) datatype: Option<Datatype>,
pub(crate) shape: Option<Vec<u64>>,
pub(crate) maxshape: Option<Vec<u64>>,
pub(crate) data: Option<Vec<u8>>,
pub(crate) attrs: Vec<(String, AttrSpec)>,
pub(crate) chunk_options: ChunkOptions,
pub(crate) raw_chunks: Option<RawChunkPayload>,
pub(crate) produced: Option<ProducedPayload>,
pub(crate) reference_targets: Option<Vec<ObjectRefPatch>>,
pub(crate) vl_string_staging: Option<VlStringStaging>,
pub(crate) fill: Option<Vec<u8>>,
pub(crate) allocation: StorageAllocation,
pub(crate) datatype_location: DatatypeLocation,
#[cfg(feature = "provenance")]
pub(crate) provenance: Option<ProvenanceConfig>,
}
impl DatasetBuilder {
pub(crate) fn new(name: &str) -> Self {
Self {
name: name.to_string(),
datatype: None,
shape: None,
maxshape: None,
data: None,
attrs: Vec::new(),
chunk_options: ChunkOptions::default(),
raw_chunks: None,
produced: None,
reference_targets: None,
vl_string_staging: None,
fill: None,
allocation: StorageAllocation::Allocated,
datatype_location: DatatypeLocation::Inline,
#[cfg(feature = "provenance")]
provenance: None,
}
}
pub fn with_committed_datatype(&mut self, path: &str) -> &mut Self {
self.datatype_location = DatatypeLocation::CommittedPath(normalize_object_path(path));
self
}
pub fn set_attr_committed(&mut self, name: &str, value: AttrValue, path: &str) -> &mut Self {
self.set_attr_verbatim(committed_attr_message(name, &value, path))
}
pub fn with_f64_data(&mut self, data: &[f64]) -> &mut Self {
self.datatype = Some(make_f64_type());
let mut b = Vec::with_capacity(data.len() * 8);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_f32_data(&mut self, data: &[f32]) -> &mut Self {
self.datatype = Some(make_f32_type());
let mut b = Vec::with_capacity(data.len() * 4);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_i32_data(&mut self, data: &[i32]) -> &mut Self {
self.datatype = Some(make_i32_type());
let mut b = Vec::with_capacity(data.len() * 4);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_i64_data(&mut self, data: &[i64]) -> &mut Self {
self.datatype = Some(make_i64_type());
let mut b = Vec::with_capacity(data.len() * 8);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_u8_data(&mut self, data: &[u8]) -> &mut Self {
self.datatype = Some(make_u8_type());
self.set_element_bytes(data.to_vec());
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_i8_data(&mut self, data: &[i8]) -> &mut Self {
self.datatype = Some(make_i8_type());
let mut b = Vec::with_capacity(data.len());
for &v in data {
b.push(v as u8);
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_i16_data(&mut self, data: &[i16]) -> &mut Self {
self.datatype = Some(make_i16_type());
let mut b = Vec::with_capacity(data.len() * 2);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_u16_data(&mut self, data: &[u16]) -> &mut Self {
self.datatype = Some(make_u16_type());
let mut b = Vec::with_capacity(data.len() * 2);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_u32_data(&mut self, data: &[u32]) -> &mut Self {
self.datatype = Some(make_u32_type());
let mut b = Vec::with_capacity(data.len() * 4);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_u64_data(&mut self, data: &[u64]) -> &mut Self {
self.datatype = Some(make_u64_type());
let mut b = Vec::with_capacity(data.len() * 8);
for &v in data {
b.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![data.len() as u64]);
}
self
}
pub fn with_reference_data(&mut self, addresses: &[u64]) -> &mut Self {
self.datatype = Some(make_object_reference_type());
let mut b = Vec::with_capacity(addresses.len() * 8);
for &addr in addresses {
b.extend_from_slice(&addr.to_le_bytes());
}
self.set_element_bytes(b);
if self.shape.is_none() {
self.shape = Some(vec![addresses.len() as u64]);
}
self
}
pub fn with_path_references(&mut self, paths: &[&str]) -> &mut Self {
let targets = paths
.iter()
.map(|s| ObjectRefTarget::Path(s.to_string()))
.collect();
self.with_object_references(targets)
}
pub(crate) fn with_object_references(&mut self, targets: Vec<ObjectRefTarget>) -> &mut Self {
self.datatype = Some(make_object_reference_type());
self.set_element_bytes(vec![0u8; targets.len() * 8]);
if self.shape.is_none() {
self.shape = Some(vec![targets.len() as u64]);
}
self.reference_targets = Some(
targets
.into_iter()
.enumerate()
.map(|(i, target)| ObjectRefPatch {
byte_offset: i * 8,
target,
})
.collect(),
);
self
}
pub(crate) fn with_embedded_object_references(
&mut self,
datatype: Datatype,
raw: Vec<u8>,
num_elements: u64,
patches: Vec<ObjectRefPatch>,
) -> &mut Self {
self.datatype = Some(datatype);
self.set_element_bytes(raw);
if self.shape.is_none() {
self.shape = Some(vec![num_elements]);
}
self.reference_targets = Some(patches);
self
}
pub fn with_complex32_data(&mut self, data: &[(f32, f32)]) -> &mut Self {
self.with_complex_data(data)
}
pub fn with_complex64_data(&mut self, data: &[(f64, f64)]) -> &mut Self {
self.with_complex_data(data)
}
pub(crate) fn with_complex_data<T: ComplexComponent>(&mut self, data: &[(T, T)]) -> &mut Self {
let ct = CompoundTypeBuilder::new()
.field("real", T::datatype())
.field("imag", T::datatype())
.build()
.expect("two fields of a nonzero-width component");
let width = size_of::<T>();
let mut raw = vec![0u8; data.len() * 2 * width];
for (slot, &(re, im)) in raw.chunks_exact_mut(2 * width).zip(data) {
let (real, imag) = slot.split_at_mut(width);
re.encode_le_into(real);
im.encode_le_into(imag);
}
self.with_compound_data(ct, raw, data.len() as u64)
}
pub fn with_compound_data(
&mut self,
datatype: Datatype,
raw_data: Vec<u8>,
num_elements: u64,
) -> &mut Self {
self.with_raw_data(datatype, raw_data, num_elements)
}
pub fn with_raw_data(
&mut self,
datatype: Datatype,
raw_data: Vec<u8>,
num_elements: u64,
) -> &mut Self {
self.datatype = Some(datatype);
self.set_element_bytes(raw_data);
if self.shape.is_none() {
self.shape = Some(vec![num_elements]);
}
self
}
fn set_element_bytes(&mut self, data: Vec<u8>) {
self.data = Some(data);
self.vl_string_staging = None;
self.reference_targets = None;
}
pub(crate) fn with_unallocated_storage(
&mut self,
datatype: Datatype,
dims: &[u64],
) -> &mut Self {
self.datatype = Some(datatype);
if self.shape.is_none() {
self.shape = Some(dims.to_vec());
}
self.allocation = StorageAllocation::Unallocated;
self
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn with_raw_chunks_lazy(
&mut self,
datatype: Datatype,
dims: &[u64],
maxshape: Option<&[u64]>,
chunk_dims: &[u64],
element_size: NonZeroUsize,
pipeline_message: Option<Vec<u8>>,
meta: Vec<ChunkMeta>,
provider: Box<dyn ChunkProvider>,
) -> &mut Self {
self.datatype = Some(datatype);
if self.shape.is_none() {
self.shape = Some(dims.to_vec());
}
if let Some(ms) = maxshape {
self.maxshape = Some(ms.to_vec());
}
self.chunk_options.chunk_dims = Some(chunk_dims.to_vec());
self.raw_chunks = Some(RawChunkPayload {
chunk_dims: chunk_dims.to_vec(),
element_size,
pipeline_message,
meta,
provider: core::panic::AssertUnwindSafe(provider),
});
self
}
pub(crate) fn with_produced_data(
&mut self,
datatype: Datatype,
shape: &[u64],
total_bytes: u64,
block_bytes: u64,
provider: Box<dyn ChunkProvider>,
) -> &mut Self {
debug_assert!(block_bytes > 0, "a block must make progress");
debug_assert!(
self.vl_string_staging.is_none()
&& self.reference_targets.is_none()
&& self.maxshape.is_none()
&& !self.chunk_options.is_chunked(),
"a produced dataset is plain contiguous storage: no VL staging, \
references, maxshape, or chunking"
);
self.datatype = Some(datatype);
if self.shape.is_none() {
self.shape = Some(shape.to_vec());
}
self.produced = Some(ProducedPayload {
total_bytes,
block_bytes,
provider: core::panic::AssertUnwindSafe(provider),
});
self
}
pub fn with_compound_values<T: CompoundType>(
&mut self,
values: &[T],
) -> Result<&mut Self, crate::error::FormatError> {
let datatype = T::datatype()?;
if !matches!(datatype, Datatype::Compound { .. }) {
return Err(crate::error::FormatError::TypeMismatch {
expected: "Compound",
actual: "non-Compound",
});
}
let element_size = datatype.type_size().to_usize()?;
if element_size == 0 {
return Err(crate::error::FormatError::InvalidCompoundSize);
}
let mut raw = Vec::with_capacity(values.len().saturating_mul(element_size));
for value in values {
let start = raw.len();
value.encode(&mut raw);
let actual = raw.len() - start;
if actual != element_size {
return Err(crate::error::FormatError::DataSizeMismatch {
expected: element_size,
actual,
});
}
}
Ok(self.with_compound_data(datatype, raw, values.len() as u64))
}
pub fn with_enum_i32_data(&mut self, datatype: Datatype, values: &[i32]) -> &mut Self {
self.datatype = Some(datatype);
let mut raw = Vec::with_capacity(values.len() * 4);
for &v in values {
raw.extend_from_slice(&v.to_le_bytes());
}
self.set_element_bytes(raw);
if self.shape.is_none() {
self.shape = Some(vec![values.len() as u64]);
}
self
}
pub fn with_enum_u8_data(&mut self, datatype: Datatype, values: &[u8]) -> &mut Self {
self.datatype = Some(datatype);
self.set_element_bytes(values.to_vec());
if self.shape.is_none() {
self.shape = Some(vec![values.len() as u64]);
}
self
}
pub fn with_vlen_strings(&mut self, values: &[&str]) -> &mut Self {
self.stage_vlen(
make_vlen_string_type(CharacterSet::Utf8),
values.len() as u64,
stage_vl_payloads(values.iter().map(|s| Some(s.as_bytes())), NonZeroUsize::MIN),
);
self
}
pub fn with_ascii_strings(&mut self, values: &[&str]) -> Result<&mut Self, FormatError> {
self.stage_fixed_strings(values, derived_string_width(values), CharacterSet::Ascii)
}
pub fn with_ascii_strings_sized(
&mut self,
values: &[&str],
width: u32,
) -> Result<&mut Self, FormatError> {
let width = checked_width(values, width)?;
self.stage_fixed_strings(values, width, CharacterSet::Ascii)
}
pub fn with_strings(&mut self, values: &[&str]) -> Result<&mut Self, FormatError> {
self.stage_fixed_strings(values, derived_string_width(values), CharacterSet::Utf8)
}
pub fn with_strings_sized(
&mut self,
values: &[&str],
width: u32,
) -> Result<&mut Self, FormatError> {
let width = checked_width(values, width)?;
self.stage_fixed_strings(values, width, CharacterSet::Utf8)
}
fn stage_fixed_strings(
&mut self,
values: &[&str],
width: NonZeroU32,
charset: CharacterSet,
) -> Result<&mut Self, FormatError> {
let raw = encode_fixed_strings(values, width)?;
self.datatype = Some(Datatype::String {
size: width.get(),
padding: StringPadding::NullPad,
charset,
});
self.set_element_bytes(raw);
if self.shape.is_none() {
self.shape = Some(vec![values.len() as u64]);
}
Ok(self)
}
pub(crate) fn with_vlen_string_elements(
&mut self,
datatype: Datatype,
elements: &[VlStringElement],
) -> Result<&mut Self, crate::error::FormatError> {
if !crate::vl_data::is_vlen_string_datatype(&datatype) {
return Err(crate::error::FormatError::TypeMismatch {
expected: "VariableLength string",
actual: "non-VariableLength string",
});
}
self.stage_vlen_strings(datatype, elements);
Ok(self)
}
fn stage_vlen_strings(&mut self, datatype: Datatype, elements: &[VlStringElement]) {
self.stage_vlen_elements(datatype, elements, NonZeroUsize::MIN);
}
pub(crate) fn with_vlen_sequence_elements(
&mut self,
datatype: Datatype,
elements: &[VlStringElement],
) -> Result<&mut Self, crate::error::FormatError> {
let Datatype::VariableLength { base_type, .. } = &datatype else {
return Err(crate::error::FormatError::TypeMismatch {
expected: "non-string VariableLength",
actual: "non-VariableLength",
});
};
if crate::vl_data::is_vlen_string_datatype(&datatype) {
return Err(crate::error::FormatError::TypeMismatch {
expected: "non-string VariableLength",
actual: "VariableLength string",
});
}
let Some(element_size) = NonZeroUsize::new(base_type.type_size() as usize) else {
return Err(crate::error::FormatError::VlDataError(
"non-string VL base type has zero size".into(),
));
};
self.stage_vlen_elements(datatype, elements, element_size);
Ok(self)
}
pub(crate) fn with_embedded_vlen_elements(
&mut self,
datatype: Datatype,
raw: Vec<u8>,
num_elements: u64,
offsets: &[usize],
elements: &[VlStringElement],
) -> &mut Self {
let (element_bytes, staging) = stage_embedded_vl_elements(raw, offsets, elements);
self.datatype = Some(datatype);
self.set_element_bytes(element_bytes);
self.vl_string_staging = Some(staging);
if self.shape.is_none() {
self.shape = Some(vec![num_elements]);
}
self
}
fn stage_vlen_elements(
&mut self,
datatype: Datatype,
elements: &[VlStringElement],
element_size: NonZeroUsize,
) {
let n = elements.len() as u64;
self.stage_vlen(datatype, n, stage_vl_elements(elements, element_size));
}
fn stage_vlen(
&mut self,
datatype: Datatype,
num_elements: u64,
(element_bytes, staging): (Vec<u8>, VlStringStaging),
) {
self.datatype = Some(datatype);
self.set_element_bytes(element_bytes);
self.vl_string_staging = Some(staging);
if self.shape.is_none() {
self.shape = Some(vec![num_elements]);
}
}
pub fn with_array_data(
&mut self,
base_type: Datatype,
array_dims: &[u32],
raw_data: Vec<u8>,
num_elements: u64,
) -> &mut Self {
self.datatype = Some(Datatype::Array {
base_type: Box::new(base_type),
dimensions: array_dims.to_vec(),
});
self.set_element_bytes(raw_data);
if self.shape.is_none() {
self.shape = Some(vec![num_elements]);
}
self
}
pub fn with_shape(&mut self, shape: &[u64]) -> &mut Self {
self.shape = Some(shape.to_vec());
self
}
pub fn with_dtype(&mut self, dt: Datatype) -> &mut Self {
self.datatype = Some(dt);
self
}
pub fn with_maxshape(&mut self, maxshape: &[u64]) -> &mut Self {
self.maxshape = Some(maxshape.to_vec());
self
}
pub fn set_attr(&mut self, name: &str, value: AttrValue) -> &mut Self {
self.attrs.push((name.to_string(), AttrSpec::Value(value)));
self
}
pub(crate) fn set_attr_verbatim(&mut self, message: AttributeMessage) -> &mut Self {
self.attrs
.push((message.name.clone(), AttrSpec::Verbatim(message)));
self
}
pub(crate) fn set_attr_var_len_verbatim(
&mut self,
mut message: AttributeMessage,
strings: Vec<String>,
) -> &mut Self {
message.raw_data = vl_string_reference_bytes(&strings);
self.attrs.push((
message.name.clone(),
AttrSpec::VerbatimVarLen { message, strings },
));
self
}
pub fn with_chunks(&mut self, chunk_dims: &[u64]) -> &mut Self {
self.chunk_options.chunk_dims = Some(chunk_dims.to_vec());
self
}
pub fn with_deflate(&mut self, level: u32) -> &mut Self {
self.chunk_options.set_filter(FilterKind::Deflate(level));
self
}
pub fn with_shuffle(&mut self) -> &mut Self {
self.chunk_options.set_filter(FilterKind::Shuffle);
self
}
pub fn with_lzf(&mut self) -> &mut Self {
self.chunk_options.set_filter(FilterKind::Lzf);
self
}
pub fn with_fletcher32(&mut self) -> &mut Self {
self.chunk_options.set_filter(FilterKind::Fletcher32);
self
}
pub fn with_scale_offset(&mut self, mode: ScaleOffset) -> &mut Self {
self.chunk_options
.set_filter(FilterKind::ScaleOffset(mode, FillAvailability::Defined));
self
}
#[cfg(feature = "zfp")]
pub fn with_zfp(&mut self, rate: f64) -> &mut Self {
self.chunk_options.set_filter(FilterKind::Zfp(rate));
self
}
#[cfg(feature = "provenance")]
pub fn with_provenance(
&mut self,
creator: &str,
timestamp: &str,
source: Option<&str>,
) -> &mut Self {
self.provenance = Some(ProvenanceConfig {
creator: creator.to_string(),
timestamp: timestamp.to_string(),
source: source.map(|s| s.to_string()),
});
self
}
}
pub struct GroupBuilder {
pub(crate) name: String,
pub(crate) datasets: Vec<DatasetBuilder>,
pub(crate) sub_groups: Vec<FinishedGroup>,
pub(crate) attrs: Vec<(String, AttrSpec)>,
pub(crate) committed: Vec<CommittedDatatype>,
}
impl GroupBuilder {
pub(crate) fn new(name: &str) -> Self {
Self {
name: name.to_string(),
datasets: Vec::new(),
sub_groups: Vec::new(),
attrs: Vec::new(),
committed: Vec::new(),
}
}
pub fn create_dataset(&mut self, name: &str) -> &mut DatasetBuilder {
self.datasets.push(DatasetBuilder::new(name));
self.datasets.last_mut().unwrap()
}
pub fn create_group(&mut self, name: &str) -> GroupBuilder {
GroupBuilder::new(name)
}
pub fn add_group(&mut self, group: FinishedGroup) {
self.sub_groups.push(group);
}
pub fn set_attr(&mut self, name: &str, value: AttrValue) {
self.attrs.push((name.to_string(), AttrSpec::Value(value)));
}
pub(crate) fn set_attr_verbatim(&mut self, message: AttributeMessage) {
self.attrs
.push((message.name.clone(), AttrSpec::Verbatim(message)));
}
pub fn set_attr_committed(&mut self, name: &str, value: AttrValue, path: &str) {
self.set_attr_verbatim(committed_attr_message(name, &value, path));
}
pub(crate) fn set_attr_var_len_verbatim(
&mut self,
mut message: AttributeMessage,
strings: Vec<String>,
) {
message.raw_data = vl_string_reference_bytes(&strings);
self.attrs.push((
message.name.clone(),
AttrSpec::VerbatimVarLen { message, strings },
));
}
pub fn commit_datatype(&mut self, name: &str, datatype: Datatype) {
self.committed.push(CommittedDatatype {
name: name.to_string(),
datatype,
});
}
pub fn finish(self) -> FinishedGroup {
FinishedGroup {
name: self.name,
datasets: self.datasets,
sub_groups: self.sub_groups,
attrs: self.attrs,
committed: self.committed,
}
}
}
pub struct FinishedGroup {
pub(crate) name: String,
pub(crate) datasets: Vec<DatasetBuilder>,
pub(crate) sub_groups: Vec<FinishedGroup>,
pub(crate) attrs: Vec<(String, AttrSpec)>,
pub(crate) committed: Vec<CommittedDatatype>,
}
pub(crate) struct CommittedDatatype {
pub(crate) name: String,
pub(crate) datatype: Datatype,
}
pub(crate) fn normalize_object_path(path: &str) -> String {
path.trim_matches('/').to_string()
}
pub(crate) fn committed_attr_message(
name: &str,
value: &AttrValue,
path: &str,
) -> AttributeMessage {
let mut message = build_attr_message(name, value);
message.datatype_location = DatatypeLocation::CommittedPath(normalize_object_path(path));
message
}
#[cfg(test)]
mod attr_value_accessor_tests {
use super::AttrValue;
#[test]
fn as_str_spans_every_single_string_shape() {
for value in [
AttrValue::String("double".into()),
AttrValue::AsciiString("double".into()),
AttrValue::StringArray(vec!["double".into()]),
AttrValue::AsciiStringArray(vec!["double".into()]),
AttrValue::VarLenAsciiArray(vec!["double".into()]),
AttrValue::string_sized("double", 32).unwrap(),
AttrValue::ascii_string_sized("double", 32).unwrap(),
AttrValue::string_array_sized(vec!["double".into()], 32).unwrap(),
AttrValue::ascii_string_array_sized(vec!["double".into()], 32).unwrap(),
] {
assert_eq!(value.as_str(), Some("double"), "{value:?}");
}
}
#[test]
fn as_str_rejects_non_single_strings() {
for value in [
AttrValue::StringArray(vec!["a".into(), "b".into()]),
AttrValue::AsciiStringArray(vec!["a".into(), "b".into()]),
AttrValue::VarLenAsciiArray(vec![]),
AttrValue::string_array_sized(vec!["a".into(), "b".into()], 8).unwrap(),
AttrValue::ascii_string_array_sized(vec![], 8).unwrap(),
AttrValue::F64(1.5),
AttrValue::I64(3),
] {
assert_eq!(value.as_str(), None, "{value:?}");
}
}
#[test]
fn as_strings_reads_a_scalar_as_one_element() {
for value in [
AttrValue::String("m/s".into()),
AttrValue::AsciiString("m/s".into()),
AttrValue::string_sized("m/s", 8).unwrap(),
AttrValue::ascii_string_sized("m/s", 8).unwrap(),
] {
let seen = value.as_strings().expect("a string value");
assert_eq!(seen, ["m/s"], "{value:?}");
assert_eq!(seen.len(), 1, "{value:?}");
}
}
#[test]
fn as_strings_keeps_every_element_of_each_array_shape() {
let fields: Vec<String> = vec!["x".into(), "y".into(), "velocity".into()];
for value in [
AttrValue::StringArray(fields.clone()),
AttrValue::AsciiStringArray(fields.clone()),
AttrValue::VarLenAsciiArray(fields.clone()),
AttrValue::string_array_sized(fields.clone(), 16).unwrap(),
AttrValue::ascii_string_array_sized(fields.clone(), 16).unwrap(),
] {
assert_eq!(
value.as_strings().expect("a string value"),
["x", "y", "velocity"],
"{value:?}"
);
}
}
#[test]
fn as_strings_separates_empty_from_absent() {
let empty = AttrValue::StringArray(vec![]);
assert_eq!(empty.as_strings(), Some(&[][..]));
assert_eq!(AttrValue::I64(1).as_strings(), None);
}
fn every_integer_variant() -> Vec<(AttrValue, i64)> {
vec![
(AttrValue::I8(-7), -7),
(AttrValue::I16(-7), -7),
(AttrValue::I32(-7), -7),
(AttrValue::I64(-7), -7),
(AttrValue::U8(7), 7),
(AttrValue::U16(7), 7),
(AttrValue::U32(7), 7),
(AttrValue::U64(7), 7),
(AttrValue::I8Array(vec![-7]), -7),
(AttrValue::I16Array(vec![-7]), -7),
(AttrValue::I32Array(vec![-7]), -7),
(AttrValue::I64Array(vec![-7]), -7),
(AttrValue::U8Array(vec![7]), 7),
(AttrValue::U16Array(vec![7]), 7),
(AttrValue::U32Array(vec![7]), 7),
(AttrValue::U64Array(vec![7]), 7),
]
}
#[test]
fn as_i64_widens_every_integer_variant() {
for (value, expected) in every_integer_variant() {
assert_eq!(value.as_i64(), Some(expected), "{}", value.type_name());
assert_eq!(
value.to_i64s(),
Some(vec![expected]),
"{} through the plural accessor",
value.type_name()
);
}
}
#[test]
fn as_u64_widens_every_integer_variant() {
for (value, expected) in every_integer_variant() {
let expected = u64::try_from(expected).ok();
assert_eq!(value.as_u64(), expected, "{}", value.type_name());
assert_eq!(
value.to_u64s(),
expected.map(|v| vec![v]),
"{} through the plural accessor",
value.type_name()
);
}
}
#[test]
fn the_widest_value_of_each_width_reads_as_itself() {
assert_eq!(AttrValue::U8(u8::MAX).as_u64(), Some(255));
assert_eq!(AttrValue::U16(u16::MAX).as_u64(), Some(65_535));
assert_eq!(AttrValue::U32(u32::MAX).as_u64(), Some(4_294_967_295));
assert_eq!(AttrValue::I8(i8::MIN).as_i64(), Some(-128));
assert_eq!(AttrValue::I16(i16::MIN).as_i64(), Some(-32_768));
assert_eq!(AttrValue::I32(i32::MIN).as_i64(), Some(-2_147_483_648));
assert_eq!(
AttrValue::U8Array(vec![u8::MAX, 0]).to_i64s(),
Some(vec![255, 0])
);
assert_eq!(
AttrValue::I16Array(vec![i16::MIN, i16::MAX]).to_i64s(),
Some(vec![-32_768, 32_767])
);
}
#[test]
fn scalar_accessors_refuse_a_value_that_does_not_fit() {
let past_max = (i64::MAX as u64) + 1;
assert_eq!(AttrValue::U64(u64::MAX).as_i64(), None);
assert_eq!(AttrValue::U64(past_max).as_i64(), None);
assert_eq!(AttrValue::U64Array(vec![past_max]).as_i64(), None);
assert_eq!(
AttrValue::U64(i64::MAX as u64).as_i64(),
Some(i64::MAX),
"the largest value that does fit must still be readable"
);
assert_eq!(AttrValue::I64(-1).as_u64(), None);
assert_eq!(AttrValue::I32(-1).as_u64(), None);
assert_eq!(AttrValue::I64Array(vec![-1]).as_u64(), None);
assert_eq!(AttrValue::I64(0).as_u64(), Some(0));
}
#[test]
fn as_i64_rejects_multi_element_and_non_integer() {
assert_eq!(AttrValue::I64Array(vec![1, 2]).as_i64(), None);
assert_eq!(AttrValue::U64Array(vec![1, 2]).as_i64(), None);
assert_eq!(AttrValue::F64(1.0).as_i64(), None);
assert_eq!(AttrValue::String("1".into()).as_i64(), None);
assert_eq!(AttrValue::F64(1.0).as_u64(), None);
}
#[test]
fn to_i64s_reads_scalars_and_arrays_alike() {
assert_eq!(AttrValue::I64(4).to_i64s(), Some(vec![4]));
assert_eq!(AttrValue::I32(4).to_i64s(), Some(vec![4]));
assert_eq!(AttrValue::U32(4).to_i64s(), Some(vec![4]));
assert_eq!(AttrValue::U64(4).to_i64s(), Some(vec![4]));
assert_eq!(
AttrValue::I64Array(vec![1, 2, 3]).to_i64s(),
Some(vec![1, 2, 3])
);
assert_eq!(AttrValue::U64Array(vec![1, 2]).to_i64s(), Some(vec![1, 2]));
assert_eq!(AttrValue::I64Array(vec![]).to_i64s(), Some(vec![]));
assert_eq!(AttrValue::U64Array(vec![]).to_i64s(), Some(vec![]));
assert_eq!(AttrValue::F64Array(vec![1.0]).to_i64s(), None);
}
#[test]
fn to_u64s_reads_scalars_and_arrays_alike() {
assert_eq!(AttrValue::U64(4).to_u64s(), Some(vec![4]));
assert_eq!(AttrValue::U32(4).to_u64s(), Some(vec![4]));
assert_eq!(AttrValue::I64(4).to_u64s(), Some(vec![4]));
assert_eq!(
AttrValue::U64Array(vec![1, u64::MAX]).to_u64s(),
Some(vec![1, u64::MAX])
);
assert_eq!(AttrValue::I64Array(vec![1, 2]).to_u64s(), Some(vec![1, 2]));
assert_eq!(AttrValue::F64(1.0).to_u64s(), None);
}
#[test]
fn plural_accessors_apply_the_range_rule_to_every_element() {
let past_max = (i64::MAX as u64) + 1;
assert_eq!(AttrValue::U64Array(vec![1, past_max]).to_i64s(), None);
assert_eq!(AttrValue::U64Array(vec![past_max, 1]).to_i64s(), None);
assert_eq!(AttrValue::U64(u64::MAX).to_i64s(), None);
assert_eq!(
AttrValue::U64Array(vec![1, i64::MAX as u64]).to_i64s(),
Some(vec![1, i64::MAX]),
"every element fitting must still read"
);
assert_eq!(AttrValue::I64Array(vec![1, -1]).to_u64s(), None);
assert_eq!(AttrValue::I64Array(vec![-1, 1]).to_u64s(), None);
assert_eq!(AttrValue::I64(-1).to_u64s(), None);
}
#[test]
fn as_f64_reads_one_float_from_either_shape() {
assert_eq!(AttrValue::F64(1.5).as_f64(), Some(1.5));
assert_eq!(AttrValue::F64Array(vec![1.5]).as_f64(), Some(1.5));
assert_eq!(AttrValue::F64Array(vec![1.5, 2.5]).as_f64(), None);
}
#[test]
fn the_float_accessors_span_both_widths() {
assert_eq!(AttrValue::F32(1.5).as_f64(), Some(1.5));
assert_eq!(AttrValue::F32Array(vec![1.5]).as_f64(), Some(1.5));
assert_eq!(AttrValue::F32Array(vec![1.5, 2.5]).as_f64(), None);
assert_eq!(AttrValue::F32(f32::MAX).as_f64(), Some(f64::from(f32::MAX)));
assert_eq!(AttrValue::F32(1.5).to_f64s(), Some(vec![1.5]));
assert_eq!(
AttrValue::F32Array(vec![f32::MIN, f32::MAX]).to_f64s(),
Some(vec![f64::from(f32::MIN), f64::from(f32::MAX)])
);
assert_eq!(AttrValue::F32Array(vec![]).to_f64s(), Some(vec![]));
assert_eq!(AttrValue::F32(1.0).as_i64(), None);
assert_eq!(AttrValue::I32(1).as_f64(), None);
}
#[test]
fn float_accessors_do_not_convert_integers() {
assert_eq!(AttrValue::I64(1).as_f64(), None);
assert_eq!(AttrValue::U32(1).as_f64(), None);
assert_eq!(AttrValue::I64Array(vec![1]).to_f64s(), None);
assert_eq!(AttrValue::U64Array(vec![1]).to_f64s(), None);
}
#[test]
fn to_f64s_reads_scalars_and_arrays_alike() {
assert_eq!(AttrValue::F64(1.5).to_f64s(), Some(vec![1.5]));
assert_eq!(
AttrValue::F64Array(vec![1.5, 2.5]).to_f64s(),
Some(vec![1.5, 2.5])
);
assert_eq!(AttrValue::F64Array(vec![]).to_f64s(), Some(vec![]));
assert_eq!(AttrValue::String("1.5".into()).to_f64s(), None);
}
}
#[cfg(all(test, feature = "std"))]
mod attr_value_display_tests {
use super::{ATTR_DISPLAY_MAX_ELEMENTS, AttrValue};
fn one_of_every_variant() -> Vec<AttrValue> {
let values = vec![
AttrValue::F32(0.0),
AttrValue::F32Array(vec![]),
AttrValue::F64(0.0),
AttrValue::F64Array(vec![]),
AttrValue::I8(0),
AttrValue::I8Array(vec![]),
AttrValue::I16(0),
AttrValue::I16Array(vec![]),
AttrValue::I32(0),
AttrValue::I32Array(vec![]),
AttrValue::I64(0),
AttrValue::I64Array(vec![]),
AttrValue::U8(0),
AttrValue::U8Array(vec![]),
AttrValue::U16(0),
AttrValue::U16Array(vec![]),
AttrValue::U32(0),
AttrValue::U32Array(vec![]),
AttrValue::U64(0),
AttrValue::U64Array(vec![]),
AttrValue::String(String::new()),
AttrValue::string_sized("", 4).unwrap(),
AttrValue::StringArray(vec![]),
AttrValue::string_array_sized(vec![], 4).unwrap(),
AttrValue::AsciiString(String::new()),
AttrValue::ascii_string_sized("", 4).unwrap(),
AttrValue::AsciiStringArray(vec![]),
AttrValue::ascii_string_array_sized(vec![], 4).unwrap(),
AttrValue::VarLenAsciiArray(vec![]),
];
for value in &values {
match value {
AttrValue::F32(_) | AttrValue::F32Array(_) => {}
AttrValue::F64(_) | AttrValue::F64Array(_) => {}
AttrValue::I8(_) | AttrValue::I8Array(_) => {}
AttrValue::I16(_) | AttrValue::I16Array(_) => {}
AttrValue::I32(_) | AttrValue::I32Array(_) => {}
AttrValue::I64(_) | AttrValue::I64Array(_) => {}
AttrValue::U8(_) | AttrValue::U8Array(_) => {}
AttrValue::U16(_) | AttrValue::U16Array(_) => {}
AttrValue::U32(_) | AttrValue::U32Array(_) => {}
AttrValue::U64(_) | AttrValue::U64Array(_) => {}
AttrValue::String(_) | AttrValue::StringArray(_) => {}
AttrValue::StringSized { .. } | AttrValue::StringArraySized { .. } => {}
AttrValue::AsciiString(_) | AttrValue::AsciiStringArray(_) => {}
AttrValue::AsciiStringSized { .. } | AttrValue::AsciiStringArraySized { .. } => {}
AttrValue::VarLenAsciiArray(_) => {}
}
}
values
}
#[test]
fn type_name_is_distinct_for_every_variant() {
let values = one_of_every_variant();
let mut names: Vec<&str> = values.iter().map(AttrValue::type_name).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "two variants share a type name");
assert!(!names.contains(&""));
}
#[test]
fn display_writes_something_for_every_variant() {
for value in one_of_every_variant() {
assert!(
!value.to_string().is_empty(),
"{} writes nothing",
value.type_name()
);
}
}
#[test]
fn display_writes_the_value_not_the_variant() {
assert_eq!(AttrValue::F64(1.5).to_string(), "1.5");
assert_eq!(AttrValue::F32(1.5).to_string(), "1.5");
assert_eq!(AttrValue::F32(1.0).to_string(), "1.0");
assert_eq!(AttrValue::I8(-7).to_string(), "-7");
assert_eq!(AttrValue::I16(-7).to_string(), "-7");
assert_eq!(AttrValue::I32(-7).to_string(), "-7");
assert_eq!(AttrValue::U8(255).to_string(), "255");
assert_eq!(AttrValue::U16(65_535).to_string(), "65535");
assert_eq!(AttrValue::U32(7).to_string(), "7");
assert_eq!(AttrValue::U8Array(vec![1, 2]).to_string(), "[1, 2]");
assert_eq!(AttrValue::I16Array(vec![-1, 2]).to_string(), "[-1, 2]");
assert_eq!(AttrValue::U64(u64::MAX).to_string(), "18446744073709551615");
assert_eq!(AttrValue::String("metres".into()).to_string(), "\"metres\"");
assert_eq!(
AttrValue::I64Array(vec![1, 2, 3]).to_string(),
"[1, 2, 3]",
"no `I64Array(..)` wrapper, which is what `Debug` is for"
);
assert_eq!(
AttrValue::StringArray(vec!["a".into(), "b".into()]).to_string(),
"[\"a\", \"b\"]"
);
assert_eq!(AttrValue::F64Array(vec![]).to_string(), "[]");
}
#[test]
fn display_keeps_the_point_on_a_whole_float() {
assert_eq!(AttrValue::F64(1.0).to_string(), "1.0");
assert_eq!(
AttrValue::F64Array(vec![1.0, 2.5]).to_string(),
"[1.0, 2.5]"
);
}
#[test]
fn display_elides_a_long_array_and_reports_the_remainder() {
let values: Vec<i64> = (0..ATTR_DISPLAY_MAX_ELEMENTS as i64 + 5).collect();
let shown = AttrValue::I64Array(values).to_string();
assert!(shown.ends_with(", … 5 more]"), "{shown}");
assert_eq!(shown.matches(", ").count(), ATTR_DISPLAY_MAX_ELEMENTS);
}
#[test]
fn display_does_not_elide_at_exactly_the_cap() {
let values: Vec<i64> = (0..ATTR_DISPLAY_MAX_ELEMENTS as i64).collect();
let shown = AttrValue::I64Array(values).to_string();
assert!(!shown.contains('…'), "{shown}");
assert!(shown.ends_with("7]"), "{shown}");
}
}
#[cfg(test)]
mod fixed_string_tests {
use super::{
AttrValue, CharacterSet, DatasetBuilder, DataspaceType, Datatype, FormatError,
StringPadding, build_attr_message, derived_string_width,
};
type Stage = fn(&mut DatasetBuilder) -> Result<&mut DatasetBuilder, FormatError>;
fn staged(builder: &DatasetBuilder) -> (Datatype, Vec<u8>) {
(
builder.datatype.clone().expect("a datatype"),
builder.data.clone().expect("element bytes"),
)
}
fn declared_width(builder: &DatasetBuilder, count: usize) -> u32 {
let (dt, raw) = staged(builder);
let Datatype::String { size, .. } = dt else {
panic!("expected a string datatype, got {dt:?}");
};
assert_eq!(
raw.len(),
size as usize * count,
"{count} elements of a {size}-byte type do not account for {} bytes",
raw.len()
);
size
}
#[test]
fn a_derived_width_is_the_longest_value_and_never_zero() {
assert_eq!(derived_string_width(&["north", "s", "east"]).get(), 5);
assert_eq!(derived_string_width(&["s", "east", "north"]).get(), 5);
assert_eq!(derived_string_width(&["é", "ab"]).get(), 2);
assert_eq!(derived_string_width::<&str>(&[]).get(), 1);
assert_eq!(derived_string_width(&["", ""]).get(), 1);
}
#[test]
fn a_derived_width_dataset_pads_every_element_to_the_longest() {
let mut b = DatasetBuilder::new("d");
b.with_ascii_strings(&["north", "s", ""]).unwrap();
assert_eq!(declared_width(&b, 3), 5);
let mut expected = Vec::new();
expected.extend_from_slice(b"north"); expected.extend_from_slice(b"s\0\0\0\0");
expected.extend_from_slice(b"\0\0\0\0\0"); assert_eq!(
staged(&b).1,
expected,
"each value is zero-padded on the right to the declared width"
);
assert_eq!(b.shape, Some(vec![3]));
}
#[test]
fn a_declared_width_is_used_verbatim_even_when_the_values_are_shorter() {
let mut b = DatasetBuilder::new("d");
b.with_ascii_strings_sized(&["ab", "c"], 4).unwrap();
assert_eq!(
declared_width(&b, 2),
4,
"the declared width stands; it is not shrunk to the values in hand"
);
assert_eq!(staged(&b).1, b"ab\0\0c\0\0\0".to_vec());
}
#[test]
fn a_value_longer_than_the_declared_width_is_refused_not_truncated() {
let mut b = DatasetBuilder::new("d");
let refused = b.with_ascii_strings_sized(&["ab", "north", "cd"], 4);
assert!(
matches!(
refused,
Err(FormatError::FixedStringTooLong {
index: 1,
len: 5,
width: 4
})
),
"{:?}",
refused.map(|_| ())
);
assert!(
b.datatype.is_none() && b.data.is_none(),
"a refused call stages nothing"
);
}
#[test]
fn a_declared_width_of_zero_is_refused() {
for refused in [
DatasetBuilder::new("d").with_ascii_strings_sized(&[""], 0),
DatasetBuilder::new("d").with_strings_sized(&[""], 0),
] {
assert!(
matches!(refused, Err(FormatError::ZeroFixedStringWidth)),
"{:?}",
refused.map(|_| ())
);
}
}
#[test]
fn each_entry_point_declares_its_own_charset_and_null_padding() {
let cases: [(Stage, _); 4] = [
(|b| b.with_ascii_strings(&["ab"]), CharacterSet::Ascii),
(
|b| b.with_ascii_strings_sized(&["ab"], 4),
CharacterSet::Ascii,
),
(|b| b.with_strings(&["ab"]), CharacterSet::Utf8),
(|b| b.with_strings_sized(&["ab"], 4), CharacterSet::Utf8),
];
for (stage, expected) in cases {
let mut b = DatasetBuilder::new("d");
stage(&mut b).unwrap();
let Datatype::String {
padding, charset, ..
} = staged(&b).0
else {
panic!("expected a string datatype");
};
assert_eq!(charset, expected);
assert_eq!(padding, StringPadding::NullPad);
}
}
#[test]
fn a_fixed_string_dataset_matches_the_attribute_encoding_of_the_same_values() {
const VALUES: [&str; 3] = ["north", "s", ""];
let owned: Vec<String> = VALUES.iter().map(|s| (*s).to_string()).collect();
let cases: [(AttrValue, Stage); 4] = [
(AttrValue::AsciiStringArray(owned.clone()), |b| {
b.with_ascii_strings(&VALUES)
}),
(AttrValue::StringArray(owned.clone()), |b| {
b.with_strings(&VALUES)
}),
(
AttrValue::ascii_string_array_sized(owned.clone(), 16).unwrap(),
|b| b.with_ascii_strings_sized(&VALUES, 16),
),
(AttrValue::string_array_sized(owned, 16).unwrap(), |b| {
b.with_strings_sized(&VALUES, 16)
}),
];
for (attr, stage) in cases {
let message = build_attr_message("a", &attr);
let mut b = DatasetBuilder::new("d");
stage(&mut b).unwrap();
let (datatype, raw) = staged(&b);
assert_eq!(datatype, message.datatype, "{attr:?}");
assert_eq!(raw, message.raw_data, "{attr:?}");
}
}
#[test]
fn an_explicit_shape_survives_a_fixed_string_write() {
let mut b = DatasetBuilder::new("d");
b.with_shape(&[2, 2]);
b.with_ascii_strings(&["a", "b", "c", "d"]).unwrap();
assert_eq!(b.shape, Some(vec![2, 2]));
assert_eq!(declared_width(&b, 4), 1);
}
#[test]
fn a_declared_attribute_width_reaches_the_message_and_the_bytes() {
let cases = [
(
AttrValue::ascii_string_sized("ok", 64).unwrap(),
CharacterSet::Ascii,
DataspaceType::Scalar,
1,
),
(
AttrValue::string_sized("ok", 64).unwrap(),
CharacterSet::Utf8,
DataspaceType::Scalar,
1,
),
(
AttrValue::ascii_string_array_sized(vec!["ok".into(), "".into()], 64).unwrap(),
CharacterSet::Ascii,
DataspaceType::Simple,
2,
),
(
AttrValue::string_array_sized(vec!["ok".into(), "".into()], 64).unwrap(),
CharacterSet::Utf8,
DataspaceType::Simple,
2,
),
];
for (value, expected_charset, expected_space, count) in cases {
let message = build_attr_message("a", &value);
let Datatype::String {
size,
padding,
charset,
} = message.datatype.clone()
else {
panic!("expected a string datatype for {value:?}");
};
assert_eq!(size, 64, "{value:?}");
assert_eq!(charset, expected_charset, "{value:?}");
assert_eq!(padding, StringPadding::NullPad, "{value:?}");
assert_eq!(message.dataspace.space_type, expected_space, "{value:?}");
assert_eq!(message.raw_data.len(), 64 * count, "{value:?}");
assert_eq!(&message.raw_data[..2], b"ok", "{value:?}");
assert!(
message.raw_data[2..].iter().all(|b| *b == 0),
"everything past the value is padding, {value:?}"
);
}
}
#[test]
fn a_plain_attribute_takes_the_content_width_and_a_sized_one_keeps_its_slot() {
let shrunk = build_attr_message("a", &AttrValue::AsciiString("x".into()));
assert!(matches!(shrunk.datatype, Datatype::String { size: 1, .. }));
let kept = build_attr_message("a", &AttrValue::ascii_string_sized("x", 5).unwrap());
assert!(matches!(kept.datatype, Datatype::String { size: 5, .. }));
}
#[test]
fn an_attribute_value_past_its_declared_width_is_refused() {
assert!(matches!(
AttrValue::ascii_string_sized("north", 2),
Err(FormatError::FixedStringTooLong {
index: 0,
len: 5,
width: 2
})
));
assert!(matches!(
AttrValue::string_sized("mètre", 5),
Err(FormatError::FixedStringTooLong {
index: 0,
len: 6,
..
})
));
assert!(matches!(
AttrValue::ascii_string_array_sized(vec!["ab".into(), "north".into()], 4),
Err(FormatError::FixedStringTooLong {
index: 1,
len: 5,
width: 4
})
));
assert!(matches!(
AttrValue::string_array_sized(vec!["ab".into(), "north".into()], 4),
Err(FormatError::FixedStringTooLong { index: 1, .. })
));
}
#[test]
fn a_declared_attribute_width_of_zero_is_refused() {
let refused = [
AttrValue::ascii_string_sized("", 0).map(|_| ()),
AttrValue::string_sized("", 0).map(|_| ()),
AttrValue::ascii_string_array_sized(vec![], 0).map(|_| ()),
AttrValue::string_array_sized(vec![], 0).map(|_| ()),
];
for outcome in refused {
assert!(
matches!(outcome, Err(FormatError::ZeroFixedStringWidth)),
"{outcome:?}"
);
}
}
}