use alloy_primitives::hex;
use bytes::Bytes;
use std::fmt;
use std::marker::PhantomData;
use crate::bmt::DEFAULT_BODY_SIZE;
use crate::cache::OnceCache;
use crate::error::{PrimitivesError, Result};
use super::bmt_body::BmtBody;
use super::traits::{BmtChunk, Chunk, ChunkAddress, ChunkHeader, ChunkMetadata};
#[derive(Debug, Clone)]
pub struct ContentChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
header: ContentChunkHeader,
body: BmtBody<BODY_SIZE>,
address_cache: OnceCache<ChunkAddress>,
}
#[derive(Debug, Clone)]
pub struct ContentChunkMetadata;
impl ChunkMetadata for ContentChunkMetadata {
fn bytes(&self) -> Bytes {
Bytes::new()
}
}
#[derive(Debug, Clone)]
pub struct ContentChunkHeader {
metadata: ContentChunkMetadata,
}
impl ContentChunkHeader {
pub const fn new() -> Self {
Self {
metadata: ContentChunkMetadata,
}
}
}
impl Default for ContentChunkHeader {
fn default() -> Self {
Self::new()
}
}
impl ChunkHeader for ContentChunkHeader {
type Metadata = ContentChunkMetadata;
fn id(&self) -> u8 {
0
}
fn version(&self) -> u8 {
1
}
fn metadata(&self) -> &Self::Metadata {
&self.metadata
}
fn bytes(&self) -> Bytes {
Bytes::new()
}
}
impl<const BODY_SIZE: usize> ContentChunk<BODY_SIZE> {
#[must_use = "this returns a new chunk without modifying the input"]
pub fn new(data: impl Into<Bytes>) -> Result<Self> {
Ok(ContentChunkBuilderImpl::<BODY_SIZE, _>::default()
.auto_from_data(data)?
.build())
}
#[must_use = "this returns a new chunk without modifying the input"]
pub fn with_address(data: impl Into<Bytes>, address: ChunkAddress) -> Result<Self> {
Ok(ContentChunkBuilderImpl::<BODY_SIZE, _>::default()
.auto_from_data(data)?
.with_address(address)
.build())
}
#[must_use]
pub const fn from_body(body: BmtBody<BODY_SIZE>) -> Self {
Self {
header: ContentChunkHeader::new(),
body,
address_cache: OnceCache::new(),
}
}
#[must_use]
pub fn from_body_with_address(body: BmtBody<BODY_SIZE>, address: ChunkAddress) -> Self {
Self {
header: ContentChunkHeader::new(),
body,
address_cache: OnceCache::with_value(address),
}
}
}
#[cfg(feature = "encryption")]
#[derive(Debug, Clone)]
pub struct EncryptedContentChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
chunk: ContentChunk<BODY_SIZE>,
encrypted_ref: super::encryption::EncryptedChunkRef,
}
#[cfg(feature = "encryption")]
impl<const BODY_SIZE: usize> EncryptedContentChunk<BODY_SIZE> {
pub const fn chunk(&self) -> &ContentChunk<BODY_SIZE> {
&self.chunk
}
pub const fn encrypted_ref(&self) -> &super::encryption::EncryptedChunkRef {
&self.encrypted_ref
}
pub fn into_parts(
self,
) -> (
ContentChunk<BODY_SIZE>,
super::encryption::EncryptedChunkRef,
) {
(self.chunk, self.encrypted_ref)
}
pub fn decrypt(&self) -> Result<ContentChunk<BODY_SIZE>> {
use super::encryption::transcrypt;
use crate::bmt::SPAN_SIZE;
let encrypted_data: Bytes = self.chunk.clone().into();
let key = self.encrypted_ref.key();
let span_ctr = (BODY_SIZE / super::encryption::EncryptionKey::SIZE) as u32;
let mut span_buf = [0u8; SPAN_SIZE];
transcrypt(key, span_ctr, &encrypted_data[..SPAN_SIZE], &mut span_buf)?;
let data_length = u64::from_le_bytes(span_buf) as usize;
let decrypted =
super::encryption::decrypt_chunk_data::<BODY_SIZE>(&encrypted_data, key, data_length)?;
ContentChunk::try_from(Bytes::from(decrypted))
}
}
#[cfg(feature = "encryption")]
impl<const BODY_SIZE: usize> super::encryption::ChunkEncrypt for ContentChunk<BODY_SIZE> {
type Encrypted = EncryptedContentChunk<BODY_SIZE>;
fn encrypt_with(
&self,
key: &super::encryption::EncryptionKey,
) -> Result<EncryptedContentChunk<BODY_SIZE>> {
let raw: Bytes = self.clone().into(); let ciphertext = super::encryption::encrypt_chunk::<BODY_SIZE>(&raw, key)?;
let encrypted_chunk = Self::try_from(Bytes::from(ciphertext))?;
let encrypted_ref =
super::encryption::EncryptedChunkRef::new(*encrypted_chunk.address(), key.clone());
Ok(EncryptedContentChunk {
chunk: encrypted_chunk,
encrypted_ref,
})
}
}
impl<const BODY_SIZE: usize> Chunk for ContentChunk<BODY_SIZE> {
type Header = ContentChunkHeader;
fn address(&self) -> &ChunkAddress {
self.address_cache.get_or_compute(|| self.body.hash())
}
fn data(&self) -> &Bytes {
self.body.data()
}
fn size(&self) -> usize {
self.header().bytes().len() + self.body.size()
}
fn header(&self) -> &Self::Header {
&self.header
}
}
impl<const BODY_SIZE: usize> BmtChunk for ContentChunk<BODY_SIZE> {
fn span(&self) -> u64 {
self.body.span()
}
}
impl<const BODY_SIZE: usize> From<ContentChunk<BODY_SIZE>> for Bytes {
fn from(chunk: ContentChunk<BODY_SIZE>) -> Self {
chunk.body.into()
}
}
impl<const BODY_SIZE: usize> TryFrom<Bytes> for ContentChunk<BODY_SIZE> {
type Error = PrimitivesError;
fn try_from(bytes: Bytes) -> Result<Self> {
Ok(Self {
header: ContentChunkHeader::new(),
body: BmtBody::try_from(bytes)?,
address_cache: OnceCache::new(),
})
}
}
impl<const BODY_SIZE: usize> TryFrom<&[u8]> for ContentChunk<BODY_SIZE> {
type Error = PrimitivesError;
fn try_from(bytes: &[u8]) -> Result<Self> {
Self::try_from(Bytes::copy_from_slice(bytes))
}
}
impl<const BODY_SIZE: usize> fmt::Display for ContentChunk<BODY_SIZE> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ContentChunk[{}]",
hex::encode(&self.address().as_bytes()[..8])
)
}
}
impl<const BODY_SIZE: usize> PartialEq for ContentChunk<BODY_SIZE> {
fn eq(&self, other: &Self) -> bool {
self.address() == other.address()
}
}
impl<const BODY_SIZE: usize> Eq for ContentChunk<BODY_SIZE> {}
impl<const BODY_SIZE: usize> super::chunk_type::ChunkType for ContentChunk<BODY_SIZE> {
const TYPE_ID: super::type_id::ChunkTypeId = super::type_id::ChunkTypeId::CONTENT;
const TYPE_NAME: &'static str = "content";
}
trait BuilderState {}
#[derive(Debug, Default)]
struct Initial;
impl BuilderState for Initial {}
#[derive(Debug)]
struct ReadyToBuild;
impl BuilderState for ReadyToBuild {}
#[derive(Debug)]
struct ContentChunkBuilderImpl<const BODY_SIZE: usize, S: BuilderState = Initial> {
body: Option<BmtBody<BODY_SIZE>>,
address: Option<ChunkAddress>,
_state: PhantomData<S>,
}
impl<const BODY_SIZE: usize> Default for ContentChunkBuilderImpl<BODY_SIZE, Initial> {
fn default() -> Self {
Self {
body: None,
address: None,
_state: PhantomData,
}
}
}
impl<const BODY_SIZE: usize> ContentChunkBuilderImpl<BODY_SIZE, Initial> {
fn auto_from_data(
mut self,
data: impl Into<Bytes>,
) -> Result<ContentChunkBuilderImpl<BODY_SIZE, ReadyToBuild>> {
let body = BmtBody::<BODY_SIZE>::builder()
.auto_from_data(data)?
.build()?;
self.body = Some(body);
Ok(ContentChunkBuilderImpl {
body: self.body,
address: self.address,
_state: PhantomData,
})
}
}
impl<const BODY_SIZE: usize> ContentChunkBuilderImpl<BODY_SIZE, ReadyToBuild> {
const fn with_address(mut self, address: ChunkAddress) -> Self {
self.address = Some(address);
self
}
fn build(self) -> ContentChunk<BODY_SIZE> {
let body = self.body.unwrap();
let address_cache = self
.address
.map_or_else(OnceCache::new, OnceCache::with_value);
ContentChunk {
header: ContentChunkHeader::new(),
body,
address_cache,
}
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<'a, const BODY_SIZE: usize> arbitrary::Arbitrary<'a> for ContentChunk<BODY_SIZE> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Self::from_body(BmtBody::<BODY_SIZE>::arbitrary(u)?))
}
}
#[cfg(test)]
mod tests {
use crate::{DEFAULT_BODY_SIZE, chunk::error::ChunkError};
use super::*;
use alloy_primitives::b256;
use proptest::prelude::*;
use proptest_arbitrary_interop::arb;
type DefaultContentChunk = ContentChunk<DEFAULT_BODY_SIZE>;
fn chunk_strategy() -> impl Strategy<Value = DefaultContentChunk> {
arb::<DefaultContentChunk>()
}
proptest! {
#[test]
fn test_chunk_properties(chunk in chunk_strategy()) {
prop_assert!(chunk.data().len() <= DEFAULT_BODY_SIZE);
prop_assert_eq!(chunk.size(), 8 + chunk.data().len());
let bytes: Bytes = chunk.clone().into();
let decoded = DefaultContentChunk::try_from(bytes).unwrap();
prop_assert_eq!(chunk.address(), decoded.address());
prop_assert_eq!(chunk.data(), decoded.data());
prop_assert_eq!(chunk.span(), decoded.span());
}
#[test]
fn test_from_body(chunk in chunk_strategy()) {
let body_data = chunk.data().clone();
let body_span = chunk.span();
let new_body = BmtBody::<DEFAULT_BODY_SIZE>::try_from(Bytes::from(chunk.clone())).unwrap();
let new_chunk = DefaultContentChunk::from_body(new_body);
prop_assert_eq!(new_chunk.data(), &body_data);
prop_assert_eq!(new_chunk.span(), body_span);
prop_assert_eq!(new_chunk.address(), chunk.address());
}
#[test]
fn test_new_content_chunk(data in proptest::collection::vec(any::<u8>(), 0..DEFAULT_BODY_SIZE)) {
let chunk = DefaultContentChunk::new(data.clone()).unwrap();
prop_assert_eq!(chunk.data(), &data);
prop_assert_eq!(chunk.span(), data.len() as u64);
prop_assert!(!chunk.address().is_zero());
}
#[test]
fn test_chunk_size_validation(data in proptest::collection::vec(any::<u8>(), DEFAULT_BODY_SIZE + 1..DEFAULT_BODY_SIZE * 2)) {
let result = DefaultContentChunk::new(data);
prop_assert_eq!(matches!(result, Err(PrimitivesError::Chunk(ChunkError::InvalidSize { .. }))), true);
}
#[test]
fn test_empty_and_edge_cases(size in 0usize..=10usize) {
let data = vec![0u8; size];
let chunk = DefaultContentChunk::new(data).unwrap();
prop_assert_eq!(chunk.data().len(), size);
prop_assert_eq!(chunk.span(), size as u64);
prop_assert_eq!(chunk.size(), 8 + size);
}
#[test]
fn test_deserialize_invalid_chunks(data in proptest::collection::vec(any::<u8>(), 0..8)) {
let result = DefaultContentChunk::try_from(data.as_slice());
prop_assert_eq!(matches!(result, Err(PrimitivesError::Chunk(ChunkError::InvalidSize { .. }))), true);
}
}
#[test]
fn test_new() {
let data = b"greaterthanspan";
let bmt_hash = b256!("27913f1bdb6e8e52cbd5a5fd4ab577c857287edf6969b41efe926b51de0f4f23");
let chunk = DefaultContentChunk::new(data.to_vec()).unwrap();
assert_eq!(chunk.address().as_ref(), bmt_hash);
assert_eq!(chunk.data(), data.as_slice());
}
#[test]
fn test_from_bytes() {
let data = b"greaterthanspan";
let bmt_hash = b256!("95022e6af5c6d6a564ee55a67f8455a3e18c511b5697c932d9e44f07f2fb8c53");
let chunk = DefaultContentChunk::try_from(data.as_slice()).unwrap();
assert_eq!(chunk.address().as_ref(), bmt_hash);
assert_eq!(
<DefaultContentChunk as Into<Bytes>>::into(chunk),
data.as_slice()
);
}
#[test]
fn test_specific_content_hash() {
let data = b"foo".to_vec();
let expected_hash =
b256!("2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48");
let chunk = DefaultContentChunk::new(data).unwrap();
assert_eq!(chunk.address().as_ref(), expected_hash);
let data = b"Digital Freedom Now".to_vec();
let chunk = DefaultContentChunk::new(data).unwrap();
assert!(chunk.address().as_ref() != ChunkAddress::default().as_ref()); }
#[test]
fn test_exact_span_size() {
let mut data = vec![0u8; 8];
data.copy_from_slice(&0u64.to_le_bytes());
let chunk = DefaultContentChunk::try_from(data.as_slice()).unwrap();
assert_eq!(chunk.span(), 0);
assert_eq!(chunk.data(), &[0u8; 0].as_slice());
assert_eq!(chunk.size(), 8);
}
}