use std::fmt;
use sha2::{Digest, Sha384};
use crate::cbor::{self, Value};
pub const DEFAULT_CHUNK_SIZE: u64 = 262_144;
pub const HASH_SIZE: usize = 48;
pub type Mcid = [u8; 50];
pub type Hash = [u8; HASH_SIZE];
pub const SHA384: &str = "sha384";
const VERSION: u32 = 1;
const TAG_SHA384: u8 = 2;
const CODEC_RAW: u8 = 0x55;
const CODEC_MANIFEST: u8 = 0x56;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChunkInfo {
pub index: u64,
pub offset: u64,
pub size: u64,
pub hash: Hash,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Manifest {
pub mcid: Mcid,
pub version: u32,
pub name: Vec<u8>,
pub size: u64,
pub created: u64,
pub chunk_size: u64,
pub chunk_count: u64,
pub hash_algorithm: String,
pub root_hash: Hash,
pub chunks: Vec<ChunkInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManifestError {
ChunkSizeZero,
ContentMismatch(String),
McidMismatch,
NotWhole(String),
ChunkHashes,
Malformed(String),
}
impl fmt::Display for ManifestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ManifestError::ChunkSizeZero => f.write_str("a chunk size of zero"),
ManifestError::ContentMismatch(why) => {
write!(f, "the content does not match the manifest: {why}")
}
ManifestError::McidMismatch => {
f.write_str("the manifest does not describe the content id")
}
ManifestError::NotWhole(why) => write!(
f,
"the manifest's chunks do not describe its content whole: {why}"
),
ManifestError::ChunkHashes => {
f.write_str("the manifest's chunk hashes do not make its root hash")
}
ManifestError::Malformed(why) => write!(f, "not a manifest: {why}"),
}
}
}
impl std::error::Error for ManifestError {}
fn sha384(data: &[u8]) -> Hash {
Sha384::digest(data).into()
}
fn make_mcid(codec: u8, hash: &Hash) -> Mcid {
let mut out = [0u8; 50];
out[0] = TAG_SHA384;
out[1] = codec;
out[2..].copy_from_slice(hash);
out
}
pub fn create(
data: &[u8],
name: &str,
chunk_size: u64,
) -> Result<(Manifest, Vec<Vec<u8>>), ManifestError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
create_at(data, name, chunk_size, now)
}
pub fn create_at(
data: &[u8],
name: &str,
chunk_size: u64,
created: u64,
) -> Result<(Manifest, Vec<Vec<u8>>), ManifestError> {
if chunk_size == 0 {
return Err(ManifestError::ChunkSizeZero);
}
let chunks: Vec<Vec<u8>> = data
.chunks(chunk_size as usize)
.map(<[u8]>::to_vec)
.collect();
let infos = chunk_infos(&chunks);
let root_hash = root_hash_for(&infos);
let mut m = Manifest {
mcid: [0; 50],
version: VERSION,
name: name.as_bytes().to_vec(),
size: data.len() as u64,
created,
chunk_size,
chunk_count: infos.len() as u64,
hash_algorithm: SHA384.into(),
root_hash,
chunks: infos,
};
m.mcid = mcid_for(&m);
Ok((m, chunks))
}
pub fn chunk_mcid(m: &Manifest, index: usize) -> Option<Mcid> {
m.chunks.get(index).map(|c| make_mcid(CODEC_RAW, &c.hash))
}
pub fn block_mcid(data: &[u8]) -> Mcid {
make_mcid(CODEC_RAW, &sha384(data))
}
pub fn mcid_is_chunked(mcid: &Mcid) -> bool {
mcid[1] == CODEC_MANIFEST
}
pub fn mcid_for(m: &Manifest) -> Mcid {
let canonical = Value::Map(vec![
(
Value::text("name"),
Value::text(String::from_utf8_lossy(&m.name)),
),
(Value::text("size"), Value::Int(i128::from(m.size))),
(
Value::text("chunk_size"),
Value::Int(i128::from(m.chunk_size)),
),
(
Value::text("chunk_count"),
Value::Int(i128::from(m.chunk_count)),
),
(
Value::text("hash_algorithm"),
Value::text(m.hash_algorithm.clone()),
),
(Value::text("root_hash"), Value::Bytes(m.root_hash.to_vec())),
]);
let encoded = cbor::encode(&canonical).expect("a manifest's canonical fields always encode");
make_mcid(CODEC_MANIFEST, &sha384(&encoded))
}
pub fn verify_mcid(m: &Manifest, mcid: &Mcid) -> Result<(), ManifestError> {
if std::str::from_utf8(&m.name).is_err() || m.hash_algorithm != SHA384 || mcid_for(m) != *mcid {
return Err(ManifestError::McidMismatch);
}
Ok(())
}
pub fn verify(m: &Manifest, data: &[u8]) -> Result<(), ManifestError> {
if m.chunk_size == 0 {
return Err(ManifestError::ChunkSizeZero);
}
if data.len() as u64 != m.size {
return Err(ManifestError::ContentMismatch(format!(
"{} bytes, the manifest's {}",
data.len(),
m.size
)));
}
let chunks: Vec<Vec<u8>> = data
.chunks(m.chunk_size as usize)
.map(<[u8]>::to_vec)
.collect();
if root_hash_for(&chunk_infos(&chunks)) != m.root_hash {
return Err(ManifestError::ContentMismatch("another root hash".into()));
}
Ok(())
}
pub fn check_whole(m: &Manifest) -> Result<(), ManifestError> {
let wanted = if m.chunk_size == 0 {
None
} else {
Some(m.size.div_ceil(m.chunk_size))
};
if wanted != Some(m.chunk_count) || m.chunk_count != m.chunks.len() as u64 {
return Err(ManifestError::NotWhole(format!(
"chunk size {}, size {}, {} chunks counted, {} listed",
m.chunk_size,
m.size,
m.chunk_count,
m.chunks.len()
)));
}
for (i, c) in m.chunks.iter().enumerate() {
let offset = i as u64 * m.chunk_size;
let size = m.chunk_size.min(m.size - offset);
if c.index != i as u64 || c.offset != offset || c.size != size {
return Err(ManifestError::NotWhole(format!("chunk {i}")));
}
}
Ok(())
}
pub fn check_chunk_hashes(m: &Manifest) -> Result<(), ManifestError> {
if root_hash_for(&m.chunks) != m.root_hash {
return Err(ManifestError::ChunkHashes);
}
Ok(())
}
fn chunk_infos(chunks: &[Vec<u8>]) -> Vec<ChunkInfo> {
let mut offset = 0u64;
chunks
.iter()
.enumerate()
.map(|(i, chunk)| {
let info = ChunkInfo {
index: i as u64,
offset,
size: chunk.len() as u64,
hash: sha384(chunk),
};
offset += chunk.len() as u64;
info
})
.collect()
}
fn root_hash_for(infos: &[ChunkInfo]) -> Hash {
let mut level: Vec<Hash> = infos.iter().map(|c| c.hash).collect();
if level.is_empty() {
return sha384(&[]);
}
while level.len() > 1 {
level = level
.chunks(2)
.map(|pair| {
let right = pair.get(1).unwrap_or(&pair[0]);
sha384(&[pair[0].as_slice(), right.as_slice()].concat())
})
.collect();
}
level[0]
}
pub fn to_wire(m: &Manifest) -> Value {
let chunks = m
.chunks
.iter()
.map(|c| {
Value::Map(vec![
(Value::text("index"), Value::Int(i128::from(c.index))),
(Value::text("offset"), Value::Int(i128::from(c.offset))),
(Value::text("size"), Value::Int(i128::from(c.size))),
(Value::text("hash"), Value::Bytes(c.hash.to_vec())),
])
})
.collect();
Value::Map(vec![
(Value::text("mcid"), Value::Bytes(m.mcid.to_vec())),
(Value::text("version"), Value::Int(i128::from(m.version))),
(Value::text("name"), Value::Bytes(m.name.clone())),
(Value::text("size"), Value::Int(i128::from(m.size))),
(Value::text("created"), Value::Int(i128::from(m.created))),
(
Value::text("chunk_size"),
Value::Int(i128::from(m.chunk_size)),
),
(
Value::text("chunk_count"),
Value::Int(i128::from(m.chunk_count)),
),
(
Value::text("hash_algorithm"),
Value::text(m.hash_algorithm.clone()),
),
(Value::text("root_hash"), Value::Bytes(m.root_hash.to_vec())),
(Value::text("chunks"), Value::List(chunks)),
])
}
pub fn from_wire(v: &Value) -> Result<Manifest, ManifestError> {
let chunks = match v.get("chunks") {
Some(Value::List(items)) => items
.iter()
.map(chunk_from_wire)
.collect::<Result<Vec<_>, _>>()?,
_ => return Err(malformed("chunks")),
};
let m = Manifest {
mcid: bytes_exact(v, "mcid")?,
version: u32::try_from(uint(v, "version")?).map_err(|_| malformed("version"))?,
name: match v.get("name") {
Some(Value::Bytes(b)) => b.clone(),
_ => return Err(malformed("name")),
},
size: uint(v, "size")?,
created: uint(v, "created")?,
chunk_size: uint(v, "chunk_size")?,
chunk_count: uint(v, "chunk_count")?,
hash_algorithm: match v.get("hash_algorithm") {
Some(Value::Text(t)) if t == SHA384 => SHA384.into(),
Some(Value::Bytes(b)) if b == SHA384.as_bytes() => SHA384.into(),
_ => return Err(malformed("hash_algorithm")),
},
root_hash: bytes_exact(v, "root_hash")?,
chunks,
};
check_whole(&m)?;
Ok(m)
}
fn chunk_from_wire(v: &Value) -> Result<ChunkInfo, ManifestError> {
Ok(ChunkInfo {
index: uint(v, "index")?,
offset: uint(v, "offset")?,
size: uint(v, "size")?,
hash: bytes_exact(v, "hash")?,
})
}
fn malformed(field: &str) -> ManifestError {
ManifestError::Malformed(format!("field {field:?} is missing or of the wrong type"))
}
fn uint(v: &Value, field: &str) -> Result<u64, ManifestError> {
match v.get(field) {
Some(Value::Int(n)) if (0..=i128::from(i64::MAX)).contains(n) => Ok(*n as u64),
_ => Err(malformed(field)),
}
}
fn bytes_exact<const N: usize>(v: &Value, field: &str) -> Result<[u8; N], ManifestError> {
match v.get(field) {
Some(Value::Bytes(b)) => b.as_slice().try_into().map_err(|_| malformed(field)),
_ => Err(malformed(field)),
}
}