use serde::{Deserialize, Serialize};
use super::cid::Cid;
use super::encoding::{
Encoding, DEFAULT_CHUNKING_FACTOR, DEFAULT_HASH_SEED, DEFAULT_MAX_CHUNK_SIZE,
DEFAULT_MIN_CHUNK_SIZE,
};
use super::error::Error;
const FORMAT_MAGIC: &[u8; 4] = b"CRFT";
const FORMAT_VERSION: u8 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChunkMeasure {
EntryCount,
LogicalBytes,
EncodedBytes,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BoundaryInput {
Key,
KeyValue,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum HashAlgorithm {
XxHash64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BoundaryRule {
HashThreshold { factor: u32 },
Weibull { shape: u32 },
RollingBuzHash { window: u16 },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChunkingSpec {
pub measure: ChunkMeasure,
pub input: BoundaryInput,
pub hash: HashAlgorithm,
pub rule: BoundaryRule,
pub min: u64,
pub target: u64,
pub max: u64,
pub hash_seed: u64,
pub level_salt: bool,
pub hard_max_node_bytes: u64,
}
impl Default for ChunkingSpec {
fn default() -> Self {
Self {
measure: ChunkMeasure::EntryCount,
input: BoundaryInput::Key,
hash: HashAlgorithm::XxHash64,
rule: BoundaryRule::HashThreshold {
factor: DEFAULT_CHUNKING_FACTOR,
},
min: DEFAULT_MIN_CHUNK_SIZE as u64,
target: DEFAULT_CHUNKING_FACTOR as u64,
max: DEFAULT_MAX_CHUNK_SIZE as u64,
hash_seed: DEFAULT_HASH_SEED,
level_salt: true,
hard_max_node_bytes: 16 * 1024 * 1024,
}
}
}
impl ChunkingSpec {
pub fn validate(&self) -> Result<(), Error> {
if self.min == 0 || self.min > self.target || self.target > self.max {
return Err(Error::InvalidFormat(
"chunk bounds must satisfy 0 < min <= target <= max".to_string(),
));
}
if self.hard_max_node_bytes == 0 {
return Err(Error::InvalidFormat(
"hard maximum node bytes must be nonzero".to_string(),
));
}
match self.rule {
BoundaryRule::HashThreshold { factor: 0 } => Err(Error::InvalidFormat(
"hash threshold factor must be nonzero".to_string(),
)),
BoundaryRule::Weibull { shape: 0 } => Err(Error::InvalidFormat(
"Weibull shape must be nonzero".to_string(),
)),
BoundaryRule::RollingBuzHash { window: 0 } => Err(Error::InvalidFormat(
"rolling hash window must be nonzero".to_string(),
)),
_ => Ok(()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeLayoutSpec {
#[default]
PrefixCompressed,
Plain,
OffsetTable,
Custom { id: String, parameters: Vec<u8> },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TreeFormat {
pub chunking: ChunkingSpec,
pub node_layout: NodeLayoutSpec,
pub value_encoding: Encoding,
}
impl Default for TreeFormat {
fn default() -> Self {
Self {
chunking: ChunkingSpec::default(),
node_layout: NodeLayoutSpec::default(),
value_encoding: Encoding::Raw,
}
}
}
impl TreeFormat {
pub fn validate(&self) -> Result<(), Error> {
self.chunking.validate()?;
if let NodeLayoutSpec::Custom { id, .. } = &self.node_layout {
if id.is_empty() {
return Err(Error::InvalidFormat(
"custom node layout identifier cannot be empty".to_string(),
));
}
}
Ok(())
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, Error> {
self.validate()?;
let mut out = Vec::new();
out.extend_from_slice(FORMAT_MAGIC);
out.push(FORMAT_VERSION);
encode_chunking(&self.chunking, &mut out);
encode_layout(&self.node_layout, &mut out);
encode_value_encoding(&self.value_encoding, &mut out);
Ok(out)
}
pub fn digest(&self) -> Result<Cid, Error> {
Ok(Cid::from_bytes(&self.canonical_bytes()?))
}
pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, Error> {
let mut cursor = FormatCursor::new(bytes);
cursor.expect(FORMAT_MAGIC)?;
if cursor.read_u8()? != FORMAT_VERSION {
return Err(Error::InvalidFormat(
"unsupported tree format version".to_string(),
));
}
let measure = match cursor.read_u8()? {
0 => ChunkMeasure::EntryCount,
1 => ChunkMeasure::LogicalBytes,
2 => ChunkMeasure::EncodedBytes,
_ => return Err(Error::InvalidFormat("invalid chunk measure".to_string())),
};
let input = match cursor.read_u8()? {
0 => BoundaryInput::Key,
1 => BoundaryInput::KeyValue,
_ => return Err(Error::InvalidFormat("invalid boundary input".to_string())),
};
let hash = match cursor.read_u8()? {
0 => HashAlgorithm::XxHash64,
_ => return Err(Error::InvalidFormat("invalid hash algorithm".to_string())),
};
let rule = match cursor.read_u8()? {
0 => BoundaryRule::HashThreshold {
factor: cursor.read_u32()?,
},
1 => BoundaryRule::Weibull {
shape: cursor.read_u32()?,
},
2 => BoundaryRule::RollingBuzHash {
window: cursor.read_u16()?,
},
_ => return Err(Error::InvalidFormat("invalid boundary rule".to_string())),
};
let chunking = ChunkingSpec {
measure,
input,
hash,
rule,
min: cursor.read_u64()?,
target: cursor.read_u64()?,
max: cursor.read_u64()?,
hash_seed: cursor.read_u64()?,
level_salt: match cursor.read_u8()? {
0 => false,
1 => true,
_ => return Err(Error::InvalidFormat("invalid level salt flag".to_string())),
},
hard_max_node_bytes: cursor.read_u64()?,
};
let node_layout = match cursor.read_u8()? {
0 => NodeLayoutSpec::PrefixCompressed,
1 => NodeLayoutSpec::Plain,
2 => NodeLayoutSpec::OffsetTable,
3 => NodeLayoutSpec::Custom {
id: cursor.read_string()?,
parameters: cursor.read_vec()?,
},
_ => return Err(Error::InvalidFormat("invalid node layout".to_string())),
};
let value_encoding = match cursor.read_u8()? {
0 => Encoding::Raw,
1 => Encoding::Cbor,
2 => Encoding::Json,
3 => Encoding::Custom(cursor.read_string()?),
_ => return Err(Error::InvalidFormat("invalid value encoding".to_string())),
};
if !cursor.is_done() {
return Err(Error::InvalidFormat(
"trailing tree format bytes".to_string(),
));
}
let format = Self {
chunking,
node_layout,
value_encoding,
};
format.validate()?;
Ok(format)
}
}
fn encode_chunking(spec: &ChunkingSpec, out: &mut Vec<u8>) {
out.push(match spec.measure {
ChunkMeasure::EntryCount => 0,
ChunkMeasure::LogicalBytes => 1,
ChunkMeasure::EncodedBytes => 2,
});
out.push(match spec.input {
BoundaryInput::Key => 0,
BoundaryInput::KeyValue => 1,
});
out.push(match spec.hash {
HashAlgorithm::XxHash64 => 0,
});
match spec.rule {
BoundaryRule::HashThreshold { factor } => {
out.push(0);
out.extend_from_slice(&factor.to_be_bytes());
}
BoundaryRule::Weibull { shape } => {
out.push(1);
out.extend_from_slice(&shape.to_be_bytes());
}
BoundaryRule::RollingBuzHash { window } => {
out.push(2);
out.extend_from_slice(&window.to_be_bytes());
}
}
out.extend_from_slice(&spec.min.to_be_bytes());
out.extend_from_slice(&spec.target.to_be_bytes());
out.extend_from_slice(&spec.max.to_be_bytes());
out.extend_from_slice(&spec.hash_seed.to_be_bytes());
out.push(u8::from(spec.level_salt));
out.extend_from_slice(&spec.hard_max_node_bytes.to_be_bytes());
}
fn encode_layout(layout: &NodeLayoutSpec, out: &mut Vec<u8>) {
match layout {
NodeLayoutSpec::PrefixCompressed => out.push(0),
NodeLayoutSpec::Plain => out.push(1),
NodeLayoutSpec::OffsetTable => out.push(2),
NodeLayoutSpec::Custom { id, parameters } => {
out.push(3);
encode_bytes(id.as_bytes(), out);
encode_bytes(parameters, out);
}
}
}
fn encode_value_encoding(encoding: &Encoding, out: &mut Vec<u8>) {
match encoding {
Encoding::Raw => out.push(0),
Encoding::Cbor => out.push(1),
Encoding::Json => out.push(2),
Encoding::Custom(name) => {
out.push(3);
encode_bytes(name.as_bytes(), out);
}
}
}
fn encode_bytes(bytes: &[u8], out: &mut Vec<u8>) {
out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
out.extend_from_slice(bytes);
}
struct FormatCursor<'a> {
bytes: &'a [u8],
position: usize,
}
impl<'a> FormatCursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, position: 0 }
}
fn expect(&mut self, expected: &[u8]) -> Result<(), Error> {
if self.read(expected.len())? != expected {
return Err(Error::InvalidFormat(
"invalid tree format magic".to_string(),
));
}
Ok(())
}
fn read(&mut self, len: usize) -> Result<&'a [u8], Error> {
let end = self
.position
.checked_add(len)
.ok_or_else(|| Error::InvalidFormat("tree format offset overflow".to_string()))?;
let value = self
.bytes
.get(self.position..end)
.ok_or_else(|| Error::InvalidFormat("truncated tree format".to_string()))?;
self.position = end;
Ok(value)
}
fn read_u8(&mut self) -> Result<u8, Error> {
Ok(self.read(1)?[0])
}
fn read_u16(&mut self) -> Result<u16, Error> {
Ok(u16::from_be_bytes(self.read(2)?.try_into().map_err(
|_| Error::InvalidFormat("invalid u16".to_string()),
)?))
}
fn read_u32(&mut self) -> Result<u32, Error> {
Ok(u32::from_be_bytes(self.read(4)?.try_into().map_err(
|_| Error::InvalidFormat("invalid u32".to_string()),
)?))
}
fn read_u64(&mut self) -> Result<u64, Error> {
Ok(u64::from_be_bytes(self.read(8)?.try_into().map_err(
|_| Error::InvalidFormat("invalid u64".to_string()),
)?))
}
fn read_vec(&mut self) -> Result<Vec<u8>, Error> {
let len = usize::try_from(self.read_u64()?)
.map_err(|_| Error::InvalidFormat("tree format length overflow".to_string()))?;
Ok(self.read(len)?.to_vec())
}
fn read_string(&mut self) -> Result<String, Error> {
String::from_utf8(self.read_vec()?)
.map_err(|_| Error::InvalidFormat("tree format string is not UTF-8".to_string()))
}
fn is_done(&self) -> bool {
self.position == self.bytes.len()
}
}