use crate::error::{Error, Result};
const MAX_DATASPACE_RANK: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataspaceType {
Scalar,
Simple,
Null,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataspaceMessage {
pub version: u8,
pub space_type: DataspaceType,
pub ndims: u8,
pub dims: Vec<u64>,
pub max_dims: Option<Vec<u64>>,
}
impl DataspaceMessage {
pub fn decode(data: &[u8]) -> Result<Self> {
Self::decode_impl(data)
}
pub fn encode(&self) -> Result<Vec<u8>> {
let mut out = Vec::new();
self.encode_into(&mut out)?;
Ok(out)
}
pub fn encode_into(&self, out: &mut Vec<u8>) -> Result<()> {
self.validate_for_encode()?;
let rank = usize::from(self.ndims);
let has_max = self.max_dims.is_some();
let header_len = match self.version {
1 => 8usize,
2 => 4usize,
_ => {
return Err(Error::InvalidFormat(format!(
"dataspace message version {}",
self.version
)))
}
};
let dim_vectors = 1usize + usize::from(has_max);
let dim_bytes = rank
.checked_mul(dim_vectors)
.and_then(|count| count.checked_mul(8))
.ok_or_else(|| Error::InvalidFormat("dataspace message size overflow".into()))?;
let capacity = header_len
.checked_add(dim_bytes)
.ok_or_else(|| Error::InvalidFormat("dataspace message size overflow".into()))?;
if out.capacity() < capacity {
out.try_reserve_exact(capacity - out.capacity())
.map_err(|err| {
Error::InvalidFormat(format!("dataspace message allocation failed: {err}"))
})?;
}
out.clear();
match self.version {
1 => {
out.push(1);
out.push(self.ndims);
out.push(if has_max { 0x01 } else { 0x00 });
out.extend_from_slice(&[0; 5]);
}
2 => {
out.push(2);
out.push(self.ndims);
out.push(if has_max { 0x01 } else { 0x00 });
out.push(self.space_type.encoded_v2_type());
}
_ => {
return Err(Error::InvalidFormat(format!(
"dataspace message version {}",
self.version
)));
}
}
write_dims(out, &self.dims);
if let Some(max_dims) = &self.max_dims {
write_dims(out, max_dims);
}
Ok(())
}
fn decode_impl(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(Error::InvalidFormat("dataspace message too short".into()));
}
let version = data[0];
let ndims = data[1];
let rank = usize::from(ndims);
if rank > MAX_DATASPACE_RANK {
return Err(Error::InvalidFormat(format!(
"dataspace rank {} exceeds supported maximum {MAX_DATASPACE_RANK}",
ndims
)));
}
match version {
1 => Self::decode_v1(data, ndims),
2 => Self::decode_v2(data, ndims),
_ => Err(Error::InvalidFormat(format!(
"dataspace message version {version}"
))),
}
}
fn decode_v1(data: &[u8], ndims: u8) -> Result<Self> {
ensure_available(data, 0, 8, "dataspace v1 header")?;
let flags = data[2];
if flags & !0x01 != 0 {
return Err(Error::Unsupported(format!(
"dataspace v1 flags {flags:#x} are not supported"
)));
}
ensure_available(data, 3, 5, "dataspace v1 reserved bytes")?;
let has_max = flags & 0x01 != 0;
let mut pos = 8;
let rank = usize::from(ndims);
let dims = read_dims(data, &mut pos, rank, "dataspace v1 dimensions")?;
let max_dims = if has_max {
let max_dims = read_dims(data, &mut pos, rank, "dataspace v1 max dimensions")?;
validate_dims_not_greater_than_max(&dims, &max_dims)?;
Some(max_dims)
} else {
None
};
let space_type = if ndims == 0 {
DataspaceType::Scalar
} else {
DataspaceType::Simple
};
let message = Self {
version: 1,
space_type,
ndims,
dims,
max_dims,
};
trace_dataspace_extent(data, flags, &message);
Ok(message)
}
fn decode_v2(data: &[u8], ndims: u8) -> Result<Self> {
let flags = data[2];
if flags & !0x01 != 0 {
return Err(Error::InvalidFormat(format!(
"dataspace v2 flags {flags:#x} are invalid"
)));
}
let space_type_val = data[3];
let space_type = match space_type_val {
0 => DataspaceType::Scalar,
1 => DataspaceType::Simple,
2 => DataspaceType::Null,
_ => {
return Err(Error::InvalidFormat(format!(
"unknown dataspace type {space_type_val}"
)))
}
};
if matches!(space_type, DataspaceType::Scalar | DataspaceType::Null) && ndims != 0 {
return Err(Error::InvalidFormat(format!(
"dataspace type {space_type:?} has rank {ndims}, expected 0"
)));
}
if space_type == DataspaceType::Simple && ndims == 0 {
return Err(Error::InvalidFormat(
"simple dataspace must have nonzero rank".into(),
));
}
let has_max = flags & 0x01 != 0;
let mut pos = 4;
let rank = usize::from(ndims);
let dims = read_dims(data, &mut pos, rank, "dataspace v2 dimensions")?;
let max_dims = if has_max {
let max_dims = read_dims(data, &mut pos, rank, "dataspace v2 max dimensions")?;
validate_dims_not_greater_than_max(&dims, &max_dims)?;
Some(max_dims)
} else {
None
};
let message = Self {
version: 2,
space_type,
ndims,
dims,
max_dims,
};
trace_dataspace_extent(data, flags, &message);
Ok(message)
}
fn validate_for_encode(&self) -> Result<()> {
if !matches!(self.version, 1 | 2) {
return Err(Error::InvalidFormat(format!(
"dataspace message version {}",
self.version
)));
}
let rank = usize::from(self.ndims);
if rank > MAX_DATASPACE_RANK {
return Err(Error::InvalidFormat(format!(
"dataspace rank {} exceeds supported maximum {MAX_DATASPACE_RANK}",
self.ndims
)));
}
if rank != self.dims.len() {
return Err(Error::InvalidFormat(format!(
"dataspace rank {} does not match {} current dimensions",
self.ndims,
self.dims.len()
)));
}
if let Some(max_dims) = &self.max_dims {
if max_dims.len() != self.dims.len() {
return Err(Error::InvalidFormat(format!(
"dataspace max rank {} does not match rank {}",
max_dims.len(),
self.dims.len()
)));
}
validate_dims_not_greater_than_max(&self.dims, max_dims)?;
}
match self.version {
1 => match self.space_type {
DataspaceType::Scalar if self.ndims == 0 => Ok(()),
DataspaceType::Simple if self.ndims != 0 => Ok(()),
DataspaceType::Null => Err(Error::Unsupported(
"dataspace v1 cannot encode null dataspaces".into(),
)),
DataspaceType::Scalar | DataspaceType::Simple => Err(Error::InvalidFormat(
"dataspace v1 extent type does not match rank".into(),
)),
},
2 => {
if matches!(self.space_type, DataspaceType::Scalar | DataspaceType::Null)
&& self.ndims != 0
{
return Err(Error::InvalidFormat(format!(
"dataspace type {:?} has rank {}, expected 0",
self.space_type, self.ndims
)));
}
if self.space_type == DataspaceType::Simple && self.ndims == 0 {
return Err(Error::InvalidFormat(
"simple dataspace must have nonzero rank".into(),
));
}
Ok(())
}
_ => Err(Error::InvalidFormat(format!(
"dataspace message version {}",
self.version
))),
}
}
}
impl DataspaceType {
fn encoded_v2_type(self) -> u8 {
match self {
DataspaceType::Scalar => 0,
DataspaceType::Simple => 1,
DataspaceType::Null => 2,
}
}
}
#[cfg(feature = "tracehash")]
fn trace_dataspace_extent(data: &[u8], flags: u8, message: &DataspaceMessage) {
let mut th = tracehash::th_call!("hdf5.dataspace.extent");
th.input_bytes(data);
th.output_value(&(true));
th.output_u64(u64::from(message.version));
th.output_u64(u64::from(message.ndims));
th.output_u64(u64::from(flags));
th.output_u64(match message.space_type {
DataspaceType::Scalar => 0,
DataspaceType::Simple => 1,
DataspaceType::Null => 2,
});
th.output_u64(u64::try_from(message.dims.len()).unwrap_or(u64::MAX));
for &dim in &message.dims {
th.output_u64(dim);
}
th.output_value(&(message.max_dims.is_some()));
if let Some(max_dims) = &message.max_dims {
th.output_u64(u64::try_from(max_dims.len()).unwrap_or(u64::MAX));
for &dim in max_dims {
th.output_u64(dim);
}
}
th.finish();
}
#[cfg(not(feature = "tracehash"))]
fn trace_dataspace_extent(_data: &[u8], _flags: u8, _message: &DataspaceMessage) {}
fn read_dims(data: &[u8], pos: &mut usize, count: usize, context: &str) -> Result<Vec<u64>> {
let bytes_len = count
.checked_mul(8)
.ok_or_else(|| Error::InvalidFormat(format!("{context} length overflow")))?;
let bytes = checked_window(data, *pos, bytes_len, context)?;
let mut dims = Vec::new();
dims.try_reserve_exact(count).map_err(|err| {
Error::InvalidFormat(format!("dataspace dimension allocation failed: {err}"))
})?;
for chunk in bytes.chunks_exact(8) {
let mut val = 0u64;
for (i, byte) in chunk.iter().enumerate() {
val |= u64::from(*byte) << (i * 8);
}
dims.push(val);
}
advance_pos(pos, bytes_len, context)?;
Ok(dims)
}
fn write_dims(out: &mut Vec<u8>, dims: &[u64]) {
for &dim in dims {
out.extend_from_slice(&dim.to_le_bytes());
}
}
fn validate_dims_not_greater_than_max(dims: &[u64], max_dims: &[u64]) -> Result<()> {
for (idx, (&dim, &max_dim)) in dims.iter().zip(max_dims).enumerate() {
if dim > max_dim {
return Err(Error::InvalidFormat(format!(
"dataspace dimension {idx} size {dim} exceeds maximum {max_dim}"
)));
}
}
Ok(())
}
fn ensure_available(data: &[u8], pos: usize, len: usize, context: &str) -> Result<()> {
let end = pos
.checked_add(len)
.ok_or_else(|| Error::InvalidFormat(format!("{context} length overflow")))?;
if end > data.len() {
return Err(Error::InvalidFormat(format!("{context} is truncated")));
}
Ok(())
}
fn checked_window<'a>(data: &'a [u8], pos: usize, len: usize, context: &str) -> Result<&'a [u8]> {
let end = pos
.checked_add(len)
.ok_or_else(|| Error::InvalidFormat(format!("{context} length overflow")))?;
data.get(pos..end)
.ok_or_else(|| Error::InvalidFormat(format!("{context} is truncated")))
}
fn advance_pos(pos: &mut usize, len: usize, context: &str) -> Result<()> {
*pos = pos
.checked_add(len)
.ok_or_else(|| Error::InvalidFormat(format!("{context} offset overflow")))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_v2_simple_with_max_dims_matches_decode_layout() {
let message = DataspaceMessage {
version: 2,
space_type: DataspaceType::Simple,
ndims: 2,
dims: vec![3, 5],
max_dims: Some(vec![10, u64::MAX]),
};
let encoded = message.encode().unwrap();
let mut expected = vec![2, 2, 1, 1];
expected.extend_from_slice(&3u64.to_le_bytes());
expected.extend_from_slice(&5u64.to_le_bytes());
expected.extend_from_slice(&10u64.to_le_bytes());
expected.extend_from_slice(&u64::MAX.to_le_bytes());
assert_eq!(encoded, expected);
assert_eq!(DataspaceMessage::decode(&encoded).unwrap(), message);
}
#[test]
fn encode_v2_scalar_and_null_have_no_dimensions() {
let scalar = DataspaceMessage {
version: 2,
space_type: DataspaceType::Scalar,
ndims: 0,
dims: Vec::new(),
max_dims: None,
};
assert_eq!(scalar.encode().unwrap(), vec![2, 0, 0, 0]);
let null = DataspaceMessage {
version: 2,
space_type: DataspaceType::Null,
ndims: 0,
dims: Vec::new(),
max_dims: None,
};
assert_eq!(null.encode().unwrap(), vec![2, 0, 0, 2]);
}
#[test]
fn encode_v1_simple_uses_reserved_header_bytes() {
let message = DataspaceMessage {
version: 1,
space_type: DataspaceType::Simple,
ndims: 1,
dims: vec![7],
max_dims: Some(vec![9]),
};
let encoded = message.encode().unwrap();
let mut expected = vec![1, 1, 1, 0, 0, 0, 0, 0];
expected.extend_from_slice(&7u64.to_le_bytes());
expected.extend_from_slice(&9u64.to_le_bytes());
assert_eq!(encoded, expected);
assert_eq!(DataspaceMessage::decode(&encoded).unwrap(), message);
}
#[test]
fn encode_rejects_invalid_dataspaces() {
let simple_rank_zero = DataspaceMessage {
version: 2,
space_type: DataspaceType::Simple,
ndims: 0,
dims: Vec::new(),
max_dims: None,
};
assert!(simple_rank_zero.encode().is_err());
let null_v1 = DataspaceMessage {
version: 1,
space_type: DataspaceType::Null,
ndims: 0,
dims: Vec::new(),
max_dims: None,
};
assert!(null_v1.encode().is_err());
let dim_exceeds_max = DataspaceMessage {
version: 2,
space_type: DataspaceType::Simple,
ndims: 1,
dims: vec![11],
max_dims: Some(vec![10]),
};
assert!(dim_exceeds_max.encode().is_err());
}
}