use crate::format::bytes::{read_le_addr as read_addr, read_le_uint as read_size};
use crate::format::{FormatContext, FormatError, FormatResult, UNDEF_ADDR};
const VERSION_1: u8 = 1;
const VERSION_2: u8 = 2;
const VERSION_3: u8 = 3;
const VERSION_4: u8 = 4;
const VERSION_5: u8 = 5;
pub const LAYOUT_VERSION_DEFAULT: u8 = VERSION_3;
const CLASS_COMPACT: u8 = 0;
const CLASS_CONTIGUOUS: u8 = 1;
const CLASS_CHUNKED: u8 = 2;
const CLASS_VIRTUAL: u8 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ChunkIndexType {
SingleChunk = 1,
Implicit = 2,
FixedArray = 3,
ExtensibleArray = 4,
BTreeV2 = 5,
}
impl ChunkIndexType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
1 => Some(Self::SingleChunk),
2 => Some(Self::Implicit),
3 => Some(Self::FixedArray),
4 => Some(Self::ExtensibleArray),
5 => Some(Self::BTreeV2),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EarrayParams {
pub max_nelmts_bits: u8,
pub idx_blk_elmts: u8,
pub sup_blk_min_data_ptrs: u8,
pub data_blk_min_elmts: u8,
pub max_dblk_page_nelmts_bits: u8,
}
impl EarrayParams {
pub fn default_params() -> Self {
Self {
max_nelmts_bits: 32,
idx_blk_elmts: 4,
sup_blk_min_data_ptrs: 4,
data_blk_min_elmts: 16,
max_dblk_page_nelmts_bits: 10,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FixedArrayParams {
pub max_dblk_page_nelmts_bits: u8,
}
impl FixedArrayParams {
pub fn default_params() -> Self {
Self {
max_dblk_page_nelmts_bits: 10,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bt2Params {
pub node_size: u32,
pub split_percent: u8,
pub merge_percent: u8,
}
impl Bt2Params {
pub fn default_params() -> Self {
use crate::format::chunk_index::btree_v2::{
BT2_MERGE_PERCENT, BT2_NODE_SIZE, BT2_SPLIT_PERCENT,
};
Self {
node_size: BT2_NODE_SIZE,
split_percent: BT2_SPLIT_PERCENT,
merge_percent: BT2_MERGE_PERCENT,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SingleChunkFilter {
pub nbytes: u64,
pub filter_mask: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DataLayoutMessage {
Contiguous {
address: u64,
size: u64,
},
Compact {
data: Vec<u8>,
},
ChunkedV3 {
chunk_dims: Vec<u64>,
b_tree_address: u64,
},
ChunkedV4 {
version: u8,
flags: u8,
chunk_dims: Vec<u64>,
index_type: ChunkIndexType,
earray_params: Option<EarrayParams>,
farray_params: Option<FixedArrayParams>,
bt2_params: Option<Bt2Params>,
single_chunk_filter: Option<SingleChunkFilter>,
index_address: u64,
},
Virtual {
version: u8,
heap_address: u64,
heap_index: u32,
},
}
impl DataLayoutMessage {
pub fn check_against_dataset(
&self,
dataspace: &crate::format::messages::dataspace::DataspaceMessage,
datatype: &crate::format::messages::datatype::DatatypeMessage,
ctx: &FormatContext,
) -> FormatResult<()> {
match self {
Self::ChunkedV3 { chunk_dims, .. } | Self::ChunkedV4 { chunk_dims, .. } => {
let rank = dataspace.dims.len();
if chunk_dims.len() != rank + 1 {
return Err(FormatError::InvalidData(format!(
"chunk dimensionality {} over a rank-{rank} dataspace; the chunk rank \
plus the element-size dimension must be {}",
chunk_dims.len(),
rank + 1
)));
}
Ok(())
}
Self::Compact { data } => {
let dt_size = datatype.element_size_ctx(ctx) as u64;
let nelmts = dataspace.element_count();
let Some(expected) = nelmts.and_then(|n| n.checked_mul(dt_size)) else {
return Err(FormatError::InvalidData(
"the size of the dataset's compact storage overflows".into(),
));
};
if data.len() as u64 != expected {
return Err(FormatError::InvalidData(format!(
"compact storage holds {} bytes but the dataset's {} elements of \
{dt_size} bytes need {expected}",
data.len(),
nelmts.unwrap_or(0)
)));
}
Ok(())
}
Self::Contiguous { .. } | Self::Virtual { .. } => Ok(()),
}
}
pub fn describe(&self) -> &'static str {
match self {
Self::Contiguous { .. } => "contiguous",
Self::Compact { .. } => "compact (version 3)",
Self::ChunkedV3 { .. } => "chunked, version-1 B-tree index (layout version 3)",
Self::ChunkedV4 { .. } => "chunked (layout version 4 or 5)",
Self::Virtual { .. } => "virtual",
}
}
pub fn contiguous_unallocated(size: u64) -> Self {
Self::Contiguous {
address: UNDEF_ADDR,
size,
}
}
pub fn contiguous(address: u64, size: u64) -> Self {
Self::Contiguous { address, size }
}
pub fn compact(data: Vec<u8>) -> Self {
Self::Compact { data }
}
pub fn chunked_v3_btree_v1(chunk_dims: Vec<u64>, b_tree_address: u64) -> Self {
Self::ChunkedV3 {
chunk_dims,
b_tree_address,
}
}
pub fn chunked_v4_earray(
version: u8,
chunk_dims: Vec<u64>,
earray_params: EarrayParams,
index_address: u64,
) -> Self {
Self::ChunkedV4 {
version,
flags: 0,
chunk_dims,
index_type: ChunkIndexType::ExtensibleArray,
earray_params: Some(earray_params),
farray_params: None,
bt2_params: None,
single_chunk_filter: None,
index_address,
}
}
pub fn chunked_v4_farray(
version: u8,
chunk_dims: Vec<u64>,
farray_params: FixedArrayParams,
index_address: u64,
) -> Self {
Self::ChunkedV4 {
version,
flags: 0,
chunk_dims,
index_type: ChunkIndexType::FixedArray,
earray_params: None,
farray_params: Some(farray_params),
bt2_params: None,
single_chunk_filter: None,
index_address,
}
}
pub fn chunked_v4_btree_v2(
version: u8,
chunk_dims: Vec<u64>,
bt2_params: Bt2Params,
index_address: u64,
) -> Self {
Self::ChunkedV4 {
version,
flags: 0,
chunk_dims,
index_type: ChunkIndexType::BTreeV2,
earray_params: None,
farray_params: None,
bt2_params: Some(bt2_params),
single_chunk_filter: None,
index_address,
}
}
pub fn chunked_v4_implicit(version: u8, chunk_dims: Vec<u64>, index_address: u64) -> Self {
Self::ChunkedV4 {
version,
flags: 0,
chunk_dims,
index_type: ChunkIndexType::Implicit,
earray_params: None,
farray_params: None,
bt2_params: None,
single_chunk_filter: None,
index_address,
}
}
pub fn virtual_layout(version: u8, heap_address: u64, heap_index: u32) -> Self {
Self::Virtual {
version,
heap_address,
heap_index,
}
}
pub fn chunked_v4_single(chunk_dims: Vec<u64>, index_address: u64) -> Self {
Self::ChunkedV4 {
version: VERSION_4,
flags: 0,
chunk_dims,
index_type: ChunkIndexType::SingleChunk,
earray_params: None,
farray_params: None,
bt2_params: None,
single_chunk_filter: None,
index_address,
}
}
pub fn chunked_v4_single_filtered(
chunk_dims: Vec<u64>,
index_address: u64,
nbytes: u64,
filter_mask: u32,
) -> Self {
Self::ChunkedV4 {
version: VERSION_4,
flags: 0x02,
chunk_dims,
index_type: ChunkIndexType::SingleChunk,
earray_params: None,
farray_params: None,
bt2_params: None,
single_chunk_filter: Some(SingleChunkFilter {
nbytes,
filter_mask,
}),
index_address,
}
}
pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
match self {
Self::Contiguous { address, size } => {
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
let mut buf = Vec::with_capacity(2 + sa + ss);
buf.push(VERSION_3);
buf.push(CLASS_CONTIGUOUS);
buf.extend_from_slice(&address.to_le_bytes()[..sa]);
buf.extend_from_slice(&size.to_le_bytes()[..ss]);
buf
}
Self::Compact { data } => {
let mut buf = Vec::with_capacity(2 + 2 + data.len());
buf.push(VERSION_3);
buf.push(CLASS_COMPACT);
buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
buf.extend_from_slice(data);
buf
}
Self::ChunkedV3 {
chunk_dims,
b_tree_address,
} => {
let sa = ctx.sizeof_addr as usize;
let ndims = chunk_dims.len() as u8;
let mut buf = Vec::with_capacity(3 + sa + chunk_dims.len() * 4);
buf.push(VERSION_3);
buf.push(CLASS_CHUNKED);
buf.push(ndims);
buf.extend_from_slice(&b_tree_address.to_le_bytes()[..sa]);
for &d in chunk_dims {
buf.extend_from_slice(&(d as u32).to_le_bytes());
}
buf
}
Self::ChunkedV4 {
version,
flags,
chunk_dims,
index_type,
earray_params,
farray_params,
bt2_params,
single_chunk_filter,
index_address,
} => {
let sa = ctx.sizeof_addr as usize;
let ndims = chunk_dims.len() as u8;
let max_dim = chunk_dims.iter().copied().max().unwrap_or(1);
let enc_bytes = enc_bytes_for_value(max_dim);
debug_assert!(matches!(*version, VERSION_4 | VERSION_5));
let mut buf = Vec::with_capacity(64);
buf.push(*version);
buf.push(CLASS_CHUNKED);
buf.push(*flags);
buf.push(ndims);
buf.push(enc_bytes);
for &d in chunk_dims {
buf.extend_from_slice(&d.to_le_bytes()[..enc_bytes as usize]);
}
buf.push(*index_type as u8);
match *index_type {
ChunkIndexType::ExtensibleArray => {
if let Some(ref params) = earray_params {
buf.push(params.max_nelmts_bits);
buf.push(params.idx_blk_elmts);
buf.push(params.sup_blk_min_data_ptrs);
buf.push(params.data_blk_min_elmts);
buf.push(params.max_dblk_page_nelmts_bits);
}
}
ChunkIndexType::FixedArray => {
if let Some(ref params) = farray_params {
buf.push(params.max_dblk_page_nelmts_bits);
}
}
ChunkIndexType::BTreeV2 => {
if let Some(ref params) = bt2_params {
buf.extend_from_slice(¶ms.node_size.to_le_bytes());
buf.push(params.split_percent);
buf.push(params.merge_percent);
}
}
ChunkIndexType::SingleChunk if *flags & 0x02 != 0 => {
if let Some(scf) = single_chunk_filter {
let ss = ctx.sizeof_size as usize;
buf.extend_from_slice(&scf.nbytes.to_le_bytes()[..ss]);
buf.extend_from_slice(&scf.filter_mask.to_le_bytes());
}
}
_ => {}
}
buf.extend_from_slice(&index_address.to_le_bytes()[..sa]);
buf
}
Self::Virtual {
version,
heap_address,
heap_index,
} => {
let sa = ctx.sizeof_addr as usize;
debug_assert!(matches!(*version, VERSION_4 | VERSION_5));
let mut buf = Vec::with_capacity(2 + sa + 4);
buf.push(*version);
buf.push(CLASS_VIRTUAL);
buf.extend_from_slice(&heap_address.to_le_bytes()[..sa]);
buf.extend_from_slice(&heap_index.to_le_bytes());
buf
}
}
}
pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
if buf.len() < 2 {
return Err(FormatError::BufferTooShort {
needed: 2,
available: buf.len(),
});
}
let version = buf[0];
let class = buf[1];
match version {
VERSION_1 | VERSION_2 => {
return Err(FormatError::UnsupportedFeature(format!(
"data layout message version {version}"
)))
}
VERSION_3 | VERSION_4 | VERSION_5 => {}
v => return Err(FormatError::InvalidVersion(v)),
}
match class {
CLASS_CONTIGUOUS => {
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
let mut pos = 2;
let needed = pos + sa + ss;
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let address = read_addr(&buf[pos..], sa);
pos += sa;
let size = read_size(&buf[pos..], ss);
pos += ss;
Ok((Self::Contiguous { address, size }, pos))
}
CLASS_COMPACT => {
let mut pos = 2;
if buf.len() < pos + 2 {
return Err(FormatError::BufferTooShort {
needed: pos + 2,
available: buf.len(),
});
}
let compact_size = u16::from_le_bytes([buf[pos], buf[pos + 1]]) as usize;
pos += 2;
if buf.len() < pos + compact_size {
return Err(FormatError::BufferTooShort {
needed: pos + compact_size,
available: buf.len(),
});
}
let data = buf[pos..pos + compact_size].to_vec();
pos += compact_size;
Ok((Self::Compact { data }, pos))
}
CLASS_CHUNKED if version == VERSION_3 => {
let sa = ctx.sizeof_addr as usize;
let mut pos = 2;
if buf.len() < pos + 1 {
return Err(FormatError::BufferTooShort {
needed: pos + 1,
available: buf.len(),
});
}
let ndims = buf[pos] as usize;
pos += 1;
if ndims < 2 {
return Err(FormatError::InvalidData(format!(
"chunked v3 layout dimensionality {ndims} is too small"
)));
}
if buf.len() < pos + sa {
return Err(FormatError::BufferTooShort {
needed: pos + sa,
available: buf.len(),
});
}
let b_tree_address = read_addr(&buf[pos..], sa);
pos += sa;
let dim_data_len = ndims * 4;
if buf.len() < pos + dim_data_len {
return Err(FormatError::BufferTooShort {
needed: pos + dim_data_len,
available: buf.len(),
});
}
let mut chunk_dims = Vec::with_capacity(ndims);
for _ in 0..ndims {
let d = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
as u64;
if d == 0 {
return Err(FormatError::InvalidData(
"chunked v3 layout has a zero chunk dimension".into(),
));
}
chunk_dims.push(d);
pos += 4;
}
Ok((
Self::ChunkedV3 {
chunk_dims,
b_tree_address,
},
pos,
))
}
CLASS_CHUNKED => {
let sa = ctx.sizeof_addr as usize;
let mut pos = 2;
if buf.len() < pos + 3 {
return Err(FormatError::BufferTooShort {
needed: pos + 3,
available: buf.len(),
});
}
let flags = buf[pos];
pos += 1;
let ndims = buf[pos] as usize;
pos += 1;
let enc_bytes = buf[pos] as usize;
pos += 1;
if !(1..=8).contains(&enc_bytes) {
return Err(FormatError::InvalidData(format!(
"chunked layout encoded dimension size {enc_bytes} is out of range"
)));
}
if ndims < 2 {
return Err(FormatError::InvalidData(format!(
"chunked v4 layout dimensionality {ndims} is too small"
)));
}
let dim_data_len = ndims * enc_bytes;
if buf.len() < pos + dim_data_len {
return Err(FormatError::BufferTooShort {
needed: pos + dim_data_len,
available: buf.len(),
});
}
let mut chunk_dims = Vec::with_capacity(ndims);
for _ in 0..ndims {
let d = read_size(&buf[pos..], enc_bytes);
if d == 0 {
return Err(FormatError::InvalidData(
"chunked v4 layout has a zero chunk dimension".into(),
));
}
chunk_dims.push(d);
pos += enc_bytes;
}
if buf.len() < pos + 1 {
return Err(FormatError::BufferTooShort {
needed: pos + 1,
available: buf.len(),
});
}
let idx_type_raw = buf[pos];
pos += 1;
let index_type = ChunkIndexType::from_u8(idx_type_raw).ok_or_else(|| {
FormatError::UnsupportedFeature(format!("chunk index type {}", idx_type_raw))
})?;
let mut earray_params = None;
let mut farray_params = None;
let mut bt2_params = None;
let mut single_chunk_filter = None;
match index_type {
ChunkIndexType::ExtensibleArray => {
if buf.len() < pos + 5 {
return Err(FormatError::BufferTooShort {
needed: pos + 5,
available: buf.len(),
});
}
let ep = EarrayParams {
max_nelmts_bits: buf[pos],
idx_blk_elmts: buf[pos + 1],
sup_blk_min_data_ptrs: buf[pos + 2],
data_blk_min_elmts: buf[pos + 3],
max_dblk_page_nelmts_bits: buf[pos + 4],
};
if ep.max_nelmts_bits == 0
|| ep.idx_blk_elmts == 0
|| ep.sup_blk_min_data_ptrs == 0
|| ep.data_blk_min_elmts == 0
|| ep.max_dblk_page_nelmts_bits == 0
{
return Err(FormatError::InvalidData(
"extensible-array layout parameter is zero".into(),
));
}
earray_params = Some(ep);
pos += 5;
}
ChunkIndexType::FixedArray => {
if buf.len() < pos + 1 {
return Err(FormatError::BufferTooShort {
needed: pos + 1,
available: buf.len(),
});
}
farray_params = Some(FixedArrayParams {
max_dblk_page_nelmts_bits: buf[pos],
});
pos += 1;
}
ChunkIndexType::BTreeV2 => {
if buf.len() < pos + 6 {
return Err(FormatError::BufferTooShort {
needed: pos + 6,
available: buf.len(),
});
}
bt2_params = Some(Bt2Params {
node_size: u32::from_le_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
]),
split_percent: buf[pos + 4],
merge_percent: buf[pos + 5],
});
pos += 6;
}
ChunkIndexType::SingleChunk if flags & 0x02 != 0 => {
let ss = ctx.sizeof_size as usize;
let extra = ss + 4;
if buf.len() < pos + extra {
return Err(FormatError::BufferTooShort {
needed: pos + extra,
available: buf.len(),
});
}
let nbytes = read_size(&buf[pos..], ss);
pos += ss;
let filter_mask = u32::from_le_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
]);
pos += 4;
single_chunk_filter = Some(SingleChunkFilter {
nbytes,
filter_mask,
});
}
_ => {}
}
if buf.len() < pos + sa {
return Err(FormatError::BufferTooShort {
needed: pos + sa,
available: buf.len(),
});
}
let index_address = read_addr(&buf[pos..], sa);
pos += sa;
Ok((
Self::ChunkedV4 {
version: buf[0],
flags,
chunk_dims,
index_type,
earray_params,
farray_params,
bt2_params,
single_chunk_filter,
index_address,
},
pos,
))
}
CLASS_VIRTUAL => {
if version == VERSION_3 {
return Err(FormatError::InvalidVersion(VERSION_3));
}
let sa = ctx.sizeof_addr as usize;
let mut pos = 2;
if buf.len() < pos + sa {
return Err(FormatError::BufferTooShort {
needed: pos + sa,
available: buf.len(),
});
}
let heap_address = read_addr(&buf[pos..], sa);
pos += sa;
if buf.len() < pos + 4 {
return Err(FormatError::BufferTooShort {
needed: pos + 4,
available: buf.len(),
});
}
let heap_index =
u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
pos += 4;
Ok((
Self::Virtual {
version: buf[0],
heap_address,
heap_index,
},
pos,
))
}
other => Err(FormatError::UnsupportedFeature(format!(
"data layout class {}",
other
))),
}
}
}
fn enc_bytes_for_value(v: u64) -> u8 {
if v == 0 {
return 1;
}
let bits_needed = 64 - v.leading_zeros(); bits_needed.div_ceil(8) as u8
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx8() -> FormatContext {
FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
}
}
fn ctx4() -> FormatContext {
FormatContext {
sizeof_addr: 4,
sizeof_size: 4,
}
}
#[test]
fn roundtrip_contiguous() {
let msg = DataLayoutMessage::contiguous(0x1000, 4096);
let encoded = msg.encode(&ctx8());
assert_eq!(encoded.len(), 18);
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(consumed, 18);
assert_eq!(decoded, msg);
}
#[test]
fn roundtrip_contiguous_ctx4() {
let msg = DataLayoutMessage::contiguous(0x800, 256);
let encoded = msg.encode(&ctx4());
assert_eq!(encoded.len(), 10);
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
assert_eq!(consumed, 10);
assert_eq!(decoded, msg);
}
#[test]
fn roundtrip_contiguous_unallocated() {
let msg = DataLayoutMessage::contiguous_unallocated(1024);
let encoded = msg.encode(&ctx8());
let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(decoded, msg);
match decoded {
DataLayoutMessage::Contiguous { address, size } => {
assert_eq!(address, UNDEF_ADDR);
assert_eq!(size, 1024);
}
_ => panic!("expected Contiguous"),
}
}
#[test]
fn roundtrip_contiguous_undef_ctx4() {
let msg = DataLayoutMessage::contiguous_unallocated(512);
let encoded = msg.encode(&ctx4());
let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
match decoded {
DataLayoutMessage::Contiguous { address, .. } => {
assert_eq!(address, UNDEF_ADDR);
}
_ => panic!("expected Contiguous"),
}
}
#[test]
fn roundtrip_compact() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
let msg = DataLayoutMessage::compact(data.clone());
let encoded = msg.encode(&ctx8());
assert_eq!(encoded.len(), 12);
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(consumed, 12);
assert_eq!(decoded, msg);
}
#[test]
fn roundtrip_compact_empty() {
let msg = DataLayoutMessage::compact(vec![]);
let encoded = msg.encode(&ctx8());
assert_eq!(encoded.len(), 4); let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(consumed, 4);
assert_eq!(decoded, msg);
}
#[test]
fn decode_legacy_version_is_unsupported_not_invalid() {
for version in [1u8, 2] {
let mut buf = vec![version, 1];
buf.extend_from_slice(&[0u8; 16]);
let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
match err {
FormatError::UnsupportedFeature(ref s) => {
assert!(s.contains(&version.to_string()), "{s}")
}
other => panic!("unexpected error for version {version}: {other:?}"),
}
}
}
#[test]
fn decode_bad_version() {
for version in [0u8, 6, 255] {
let mut buf = vec![version, 1];
buf.extend_from_slice(&[0u8; 16]);
let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
match err {
FormatError::InvalidVersion(v) if v == version => {}
other => panic!("unexpected error for version {version}: {other:?}"),
}
}
}
#[test]
fn decode_contiguous_and_compact_at_every_modern_version() {
for version in [3u8, 4, 5] {
let mut contig = vec![version, CLASS_CONTIGUOUS];
contig.extend_from_slice(&0x800u64.to_le_bytes());
contig.extend_from_slice(&64u64.to_le_bytes());
let (decoded, consumed) = DataLayoutMessage::decode(&contig, &ctx8()).unwrap();
assert_eq!(consumed, contig.len());
assert_eq!(
decoded,
DataLayoutMessage::Contiguous {
address: 0x800,
size: 64
}
);
let payload = [1u8, 2, 3, 4];
let mut compact = vec![version, CLASS_COMPACT];
compact.extend_from_slice(&(payload.len() as u16).to_le_bytes());
compact.extend_from_slice(&payload);
let (decoded, consumed) = DataLayoutMessage::decode(&compact, &ctx8()).unwrap();
assert_eq!(consumed, compact.len());
assert_eq!(
decoded,
DataLayoutMessage::Compact {
data: payload.to_vec()
}
);
}
}
#[test]
fn decode_unsupported_class() {
let buf = [3u8, 4]; let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
match err {
FormatError::UnsupportedFeature(_) => {}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn decode_buffer_too_short() {
let buf = [3u8];
let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
match err {
FormatError::BufferTooShort { .. } => {}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn decode_contiguous_truncated() {
let buf = [3u8, 1, 0, 0];
let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
match err {
FormatError::BufferTooShort { .. } => {}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn version_and_class_bytes() {
let encoded = DataLayoutMessage::contiguous(0, 0).encode(&ctx8());
assert_eq!(encoded[0], 3);
assert_eq!(encoded[1], 1);
let encoded = DataLayoutMessage::compact(vec![]).encode(&ctx8());
assert_eq!(encoded[0], 3);
assert_eq!(encoded[1], 0);
}
#[test]
fn roundtrip_chunked_v4_earray() {
let params = EarrayParams::default_params();
let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
let encoded = msg.encode(&ctx8());
assert_eq!(encoded[0], 4); assert_eq!(encoded[1], 2); let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
}
#[test]
fn roundtrip_chunked_v4_earray_ctx4() {
let params = EarrayParams::default_params();
let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 128], params, 0x1000);
let encoded = msg.encode(&ctx4());
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
}
#[test]
fn roundtrip_chunked_v5_earray() {
let params = EarrayParams::default_params();
let v5 = DataLayoutMessage::chunked_v4_earray(5, vec![1, 256, 256], params.clone(), 0x2000);
let encoded = v5.encode(&ctx8());
assert_eq!(encoded[0], 5); assert_eq!(encoded[1], 2); let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, v5);
let v4 = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
let encoded_v4 = v4.encode(&ctx8());
assert_eq!(encoded[1..], encoded_v4[1..]);
}
#[test]
fn roundtrip_chunked_v4_single() {
let msg = DataLayoutMessage::chunked_v4_single(vec![100, 200], 0x3000);
let encoded = msg.encode(&ctx8());
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
}
#[test]
fn roundtrip_chunked_v4_btree_v2_params() {
for ctx in [ctx8(), ctx4()] {
let msg = DataLayoutMessage::chunked_v4_btree_v2(
4,
vec![2, 2, 8],
Bt2Params {
node_size: 512,
split_percent: 90,
merge_percent: 30,
},
0x2000,
);
let encoded = msg.encode(&ctx);
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
}
}
#[test]
fn roundtrip_chunked_v4_single_filtered() {
for ctx in [ctx8(), ctx4()] {
let msg = DataLayoutMessage::ChunkedV4 {
version: 4,
flags: 0x02,
chunk_dims: vec![100, 200, 4],
index_type: ChunkIndexType::SingleChunk,
earray_params: None,
farray_params: None,
bt2_params: None,
single_chunk_filter: Some(SingleChunkFilter {
nbytes: 12345,
filter_mask: 0b101,
}),
index_address: 0x3000,
};
let encoded = msg.encode(&ctx);
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
match decoded {
DataLayoutMessage::ChunkedV4 {
single_chunk_filter: Some(scf),
..
} => {
assert_eq!(scf.nbytes, 12345);
assert_eq!(scf.filter_mask, 0b101);
}
other => panic!("expected filtered single-chunk layout, got {other:?}"),
}
}
}
#[test]
fn chunked_v4_enc_bytes() {
let params = EarrayParams::default_params();
let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
let encoded = msg.encode(&ctx8());
assert_eq!(encoded.len(), 25);
assert_eq!(encoded[4], 2); }
#[test]
fn roundtrip_chunked_v3_btree_v1() {
let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![8, 4], 0x1234);
let encoded = msg.encode(&ctx8());
assert_eq!(encoded.len(), 19);
assert_eq!(encoded[0], 3);
assert_eq!(encoded[1], 2);
assert_eq!(encoded[2], 2); let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
}
#[test]
fn roundtrip_chunked_v3_btree_v1_2d_ctx4() {
let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 3, 8], 0x800);
let encoded = msg.encode(&ctx4());
assert_eq!(encoded.len(), 19);
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
}
#[test]
fn chunked_v3_undef_btree_addr() {
let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![16, 4], UNDEF_ADDR);
let encoded = msg.encode(&ctx8());
let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
match decoded {
DataLayoutMessage::ChunkedV3 { b_tree_address, .. } => {
assert_eq!(b_tree_address, UNDEF_ADDR);
}
_ => panic!("expected ChunkedV3"),
}
}
#[test]
fn chunked_v3_rejects_ndims_too_small() {
let buf = [3u8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
assert!(matches!(err, FormatError::InvalidData(_)));
}
#[test]
fn chunked_v3_rejects_zero_dim() {
let mut buf = vec![3u8, 2, 2];
buf.extend_from_slice(&0u64.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&4u32.to_le_bytes()); let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
assert!(matches!(err, FormatError::InvalidData(_)));
}
#[test]
fn chunked_v3_truncated() {
let buf = [3u8, 2, 2];
let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
assert!(matches!(err, FormatError::BufferTooShort { .. }));
}
#[test]
fn roundtrip_virtual_layout() {
for ctx in [ctx8(), ctx4()] {
for version in [4u8, 5u8] {
let msg = DataLayoutMessage::virtual_layout(version, 0x5000, 3);
let encoded = msg.encode(&ctx);
assert_eq!(encoded[0], version);
assert_eq!(encoded[1], CLASS_VIRTUAL);
let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded, msg);
}
}
}
#[test]
fn virtual_layout_undefined_heap_address() {
let msg = DataLayoutMessage::virtual_layout(4, UNDEF_ADDR, 0);
let encoded = msg.encode(&ctx8());
let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
match decoded {
DataLayoutMessage::Virtual {
heap_address,
heap_index,
..
} => {
assert_eq!(heap_address, UNDEF_ADDR);
assert_eq!(heap_index, 0);
}
other => panic!("expected Virtual, got {other:?}"),
}
}
#[test]
fn virtual_layout_rejects_version_3() {
let buf = [VERSION_3, CLASS_VIRTUAL];
let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
assert!(matches!(err, FormatError::InvalidVersion(VERSION_3)));
}
#[test]
fn chunked_v4_large_dims() {
let params = EarrayParams::default_params();
let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 65536], params, 0x4000);
let encoded = msg.encode(&ctx8());
assert_eq!(encoded[4], 3); }
#[test]
fn check_against_dataset_chunk_rank_boundaries() {
use crate::format::messages::dataspace::DataspaceMessage;
use crate::format::messages::datatype::DatatypeMessage;
let ctx = FormatContext::default_v3();
let i32_t = DatatypeMessage::i32_type();
let rank2 = DataspaceMessage::simple(&[3, 4]);
let v3 = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 2, 4], 0x1000);
assert!(v3.check_against_dataset(&rank2, &i32_t, &ctx).is_ok());
let v4 = DataLayoutMessage::chunked_v4_single(vec![2, 2, 4], 0x1000);
assert!(v4.check_against_dataset(&rank2, &i32_t, &ctx).is_ok());
let rank3 = DataspaceMessage::simple(&[3, 4, 5]);
let err = v3.check_against_dataset(&rank3, &i32_t, &ctx).unwrap_err();
assert!(
matches!(err, FormatError::InvalidData(ref s) if s.contains("must be 4")),
"{err}"
);
assert!(v4.check_against_dataset(&rank3, &i32_t, &ctx).is_err());
let rank1 = DataspaceMessage::simple(&[8]);
assert!(v3.check_against_dataset(&rank1, &i32_t, &ctx).is_err());
assert!(v3
.check_against_dataset(&DataspaceMessage::scalar(), &i32_t, &ctx)
.is_err());
let contiguous = DataLayoutMessage::contiguous_unallocated(96);
assert!(contiguous
.check_against_dataset(&rank3, &i32_t, &ctx)
.is_ok());
assert!(contiguous
.check_against_dataset(&DataspaceMessage::scalar(), &i32_t, &ctx)
.is_ok());
}
#[test]
fn check_against_dataset_compact_size_boundaries() {
use crate::format::messages::dataspace::DataspaceMessage;
use crate::format::messages::datatype::DatatypeMessage;
let ctx = FormatContext::default_v3();
let i32_t = DatatypeMessage::i32_type();
let d = DataspaceMessage::simple(&[3, 4]);
let ok = DataLayoutMessage::compact(vec![0u8; 48]);
assert!(ok.check_against_dataset(&d, &i32_t, &ctx).is_ok());
let short = DataLayoutMessage::compact(vec![0u8; 44]);
let err = short.check_against_dataset(&d, &i32_t, &ctx).unwrap_err();
assert!(
matches!(err, FormatError::InvalidData(ref s) if s.contains("44 bytes") && s.contains("need 48")),
"{err}"
);
let long = DataLayoutMessage::compact(vec![0u8; 52]);
assert!(long.check_against_dataset(&d, &i32_t, &ctx).is_err());
let one = DataLayoutMessage::compact(vec![0u8; 4]);
assert!(one
.check_against_dataset(&DataspaceMessage::scalar(), &i32_t, &ctx)
.is_ok());
assert!(one
.check_against_dataset(&DataspaceMessage::null(), &i32_t, &ctx)
.is_err());
let none = DataLayoutMessage::compact(vec![]);
assert!(none
.check_against_dataset(&DataspaceMessage::null(), &i32_t, &ctx)
.is_ok());
assert!(none
.check_against_dataset(&DataspaceMessage::simple(&[0, 5]), &i32_t, &ctx)
.is_ok());
let vlen = DatatypeMessage::vlen_string_ascii();
let small_ctx = FormatContext {
sizeof_addr: 4,
sizeof_size: 4,
};
let refs = DataLayoutMessage::compact(vec![0u8; 2 * 12]);
assert!(refs
.check_against_dataset(&DataspaceMessage::simple(&[2]), &vlen, &small_ctx)
.is_ok());
assert!(refs
.check_against_dataset(&DataspaceMessage::simple(&[2]), &vlen, &ctx)
.is_err());
let huge = DataspaceMessage::simple(&[u64::MAX, 2]);
assert!(ok.check_against_dataset(&huge, &i32_t, &ctx).is_err());
let huge_bytes = DataspaceMessage::simple(&[u64::MAX / 2]);
assert!(ok.check_against_dataset(&huge_bytes, &i32_t, &ctx).is_err());
}
}