#[cfg(not(feature = "std"))]
use alloc::{format, string::String, vec::Vec};
use core::fmt;
use crate::display::{Dims, EscapedName};
use crate::error::FormatError;
use crate::filter_pipeline::{
FILTER_DEFLATE, FILTER_FLETCHER32, FILTER_LZF, FILTER_SCALEOFFSET, FILTER_SHUFFLE,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Layout {
Compact {
size: u64,
},
Contiguous {
address: Option<u64>,
size: u64,
},
Chunked {
chunk_shape: Vec<u64>,
index: ChunkIndex,
},
Virtual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ChunkIndex {
BTreeV1,
SingleChunk,
Implicit,
FixedArray,
ExtensibleArray,
BTreeV2,
}
impl ChunkIndex {
#[must_use]
pub const fn supports_inplace_append(self) -> bool {
matches!(self, ChunkIndex::ExtensibleArray)
}
pub(crate) fn from_layout(version: u8, index_type: Option<u8>) -> Result<Self, FormatError> {
Ok(match (version, index_type) {
(3, _) => ChunkIndex::BTreeV1,
(4, Some(1)) => ChunkIndex::SingleChunk,
(4, Some(2)) => ChunkIndex::Implicit,
(4, Some(3)) => ChunkIndex::FixedArray,
(4, Some(4)) => ChunkIndex::ExtensibleArray,
(4, Some(5)) => ChunkIndex::BTreeV2,
(v, Some(idx)) => {
return Err(FormatError::ChunkedReadError(format!(
"unrecognized chunk index (layout version={v}, index type={idx})"
)));
}
(v, None) => {
return Err(FormatError::ChunkedReadError(format!(
"unrecognized chunk index (layout version={v}, no index type)"
)));
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Chunk {
pub offset: Vec<u64>,
pub address: u64,
pub storage_size: u64,
pub filter_mask: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Filter {
pub id: u16,
pub name: Option<String>,
pub is_optional: bool,
pub client_data: Vec<u32>,
}
impl fmt::Display for Layout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Compact { size } => write!(f, "compact ({size} bytes)"),
Self::Contiguous {
address: Some(address),
size,
} => write!(f, "contiguous ({size} bytes at 0x{address:x})"),
Self::Contiguous {
address: None,
size,
} => write!(f, "contiguous ({size} bytes, unallocated)"),
Self::Chunked { chunk_shape, index } => {
write!(f, "chunked ({}, {index} index)", Dims(chunk_shape))
}
Self::Virtual => f.write_str("virtual"),
}
}
}
impl fmt::Display for ChunkIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(match self {
Self::BTreeV1 => "B-tree v1",
Self::SingleChunk => "single chunk",
Self::Implicit => "implicit",
Self::FixedArray => "fixed array",
Self::ExtensibleArray => "extensible array",
Self::BTreeV2 => "B-tree v2",
})
}
}
impl fmt::Display for Filter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let well_known = well_known_filter_name(self.id);
match well_known {
Some(name) => f.write_str(name)?,
None => write!(
f,
"{}",
EscapedName(self.name.as_deref().unwrap_or("filter"))
)?,
}
let write_id = well_known.is_none();
if write_id || !self.client_data.is_empty() {
f.write_str("(")?;
if write_id {
write!(f, "id={}", self.id)?;
}
for (i, value) in self.client_data.iter().enumerate() {
if write_id || i > 0 {
f.write_str(", ")?;
}
write!(f, "{value}")?;
}
f.write_str(")")?;
}
if self.is_optional {
f.write_str(" [optional]")?;
}
Ok(())
}
}
fn well_known_filter_name(id: u16) -> Option<&'static str> {
Some(match id {
FILTER_DEFLATE => "deflate",
FILTER_SHUFFLE => "shuffle",
FILTER_FLETCHER32 => "fletcher32",
FILTER_SCALEOFFSET => "scaleoffset",
FILTER_LZF => "lzf",
4 => "szip",
5 => "nbit",
32013 => "zfp",
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chunk_index_from_layout_maps_every_kind() {
assert_eq!(
ChunkIndex::from_layout(3, None).unwrap(),
ChunkIndex::BTreeV1
);
assert_eq!(
ChunkIndex::from_layout(3, Some(4)).unwrap(),
ChunkIndex::BTreeV1,
"v3 is always a v1 B-tree regardless of the index-type byte"
);
assert_eq!(
ChunkIndex::from_layout(4, Some(1)).unwrap(),
ChunkIndex::SingleChunk
);
assert_eq!(
ChunkIndex::from_layout(4, Some(2)).unwrap(),
ChunkIndex::Implicit
);
assert_eq!(
ChunkIndex::from_layout(4, Some(3)).unwrap(),
ChunkIndex::FixedArray
);
assert_eq!(
ChunkIndex::from_layout(4, Some(4)).unwrap(),
ChunkIndex::ExtensibleArray
);
assert_eq!(
ChunkIndex::from_layout(4, Some(5)).unwrap(),
ChunkIndex::BTreeV2
);
}
#[test]
fn chunk_index_from_layout_rejects_unknown() {
assert!(ChunkIndex::from_layout(4, Some(9)).is_err());
assert!(ChunkIndex::from_layout(4, None).is_err());
assert!(ChunkIndex::from_layout(2, Some(1)).is_err());
}
#[test]
fn only_extensible_array_supports_inplace_append() {
assert!(ChunkIndex::ExtensibleArray.supports_inplace_append());
for idx in [
ChunkIndex::BTreeV1,
ChunkIndex::SingleChunk,
ChunkIndex::Implicit,
ChunkIndex::FixedArray,
ChunkIndex::BTreeV2,
] {
assert!(!idx.supports_inplace_append());
}
}
}
#[cfg(all(test, feature = "std"))]
mod display_tests {
use super::*;
#[test]
fn a_layout_reads_as_one_line() {
assert_eq!(
Layout::Compact { size: 40 }.to_string(),
"compact (40 bytes)"
);
assert_eq!(
Layout::Contiguous {
address: Some(0x2a0),
size: 128,
}
.to_string(),
"contiguous (128 bytes at 0x2a0)"
);
assert_eq!(
Layout::Chunked {
chunk_shape: vec![4, 8],
index: ChunkIndex::ExtensibleArray,
}
.to_string(),
"chunked (4x8, extensible array index)"
);
}
#[test]
fn an_unallocated_contiguous_dataset_says_so() {
let layout = Layout::Contiguous {
address: None,
size: 64,
};
let shown = layout.to_string();
assert_eq!(shown, "contiguous (64 bytes, unallocated)");
assert!(!shown.contains("None"));
}
#[test]
fn a_filter_is_named_by_its_identifier_when_the_file_records_none() {
let deflate = Filter {
id: 1,
name: None,
is_optional: false,
client_data: vec![6],
};
assert_eq!(deflate.to_string(), "deflate(6)");
let lzf = Filter {
id: 32000,
name: None,
is_optional: false,
client_data: vec![],
};
assert_eq!(lzf.to_string(), "lzf");
}
#[test]
fn an_unregistered_filter_falls_back_to_its_recorded_name_then_its_id() {
let named = Filter {
id: 40000,
name: Some("custom".into()),
is_optional: true,
client_data: vec![],
};
assert_eq!(named.to_string(), "custom(id=40000) [optional]");
let anonymous = Filter {
id: 40001,
name: None,
is_optional: false,
client_data: vec![],
};
assert_eq!(anonymous.to_string(), "filter(id=40001)");
}
#[test]
fn a_recorded_filter_name_cannot_carry_a_control_character() {
let hostile = Filter {
id: 40000,
name: Some("evil\u{1b}[31m\nname".into()),
is_optional: false,
client_data: vec![],
};
let shown = hostile.to_string();
assert!(!shown.chars().any(char::is_control), "{shown}");
assert_eq!(shown, "evil\\u{1b}[31m\\nname(id=40000)");
}
#[cfg(feature = "zfp")]
#[test]
fn the_zfp_name_is_reached_through_its_own_identifier() {
assert_eq!(
well_known_filter_name(crate::filter_pipeline::FILTER_ZFP),
Some("zfp")
);
}
#[test]
fn an_unregistered_filter_keeps_its_id_apart_from_its_client_data() {
let named = Filter {
id: 40000,
name: Some("custom".into()),
is_optional: false,
client_data: vec![7, 8],
};
assert_eq!(named.to_string(), "custom(id=40000, 7, 8)");
}
#[test]
fn an_unrecognized_index_error_has_no_rust_option_in_it() {
let with_type = ChunkIndex::from_layout(4, Some(9)).unwrap_err().to_string();
assert!(with_type.contains("index type=9"), "{with_type}");
assert!(!with_type.contains("Some"), "{with_type}");
let without_type = ChunkIndex::from_layout(9, None).unwrap_err().to_string();
assert!(without_type.contains("no index type"), "{without_type}");
assert!(!without_type.contains("None"), "{without_type}");
}
}