1use std::fmt;
12
13use sha2::{Digest, Sha384};
14
15use crate::cbor::{self, Value};
16
17pub const DEFAULT_CHUNK_SIZE: u64 = 262_144;
19
20pub const HASH_SIZE: usize = 48;
22
23pub type Mcid = [u8; 50];
25
26pub type Hash = [u8; HASH_SIZE];
28
29pub const SHA384: &str = "sha384";
31
32const VERSION: u32 = 1;
33const TAG_SHA384: u8 = 2;
34const CODEC_RAW: u8 = 0x55;
35const CODEC_MANIFEST: u8 = 0x56;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ChunkInfo {
40 pub index: u64,
41 pub offset: u64,
42 pub size: u64,
43 pub hash: Hash,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Manifest {
51 pub mcid: Mcid,
52 pub version: u32,
53 pub name: Vec<u8>,
54 pub size: u64,
55 pub created: u64,
57 pub chunk_size: u64,
58 pub chunk_count: u64,
59 pub hash_algorithm: String,
60 pub root_hash: Hash,
61 pub chunks: Vec<ChunkInfo>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum ManifestError {
67 ChunkSizeZero,
69 ContentMismatch(String),
71 McidMismatch,
73 NotWhole(String),
75 ChunkHashes,
77 Malformed(String),
79}
80
81impl fmt::Display for ManifestError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 ManifestError::ChunkSizeZero => f.write_str("a chunk size of zero"),
85 ManifestError::ContentMismatch(why) => {
86 write!(f, "the content does not match the manifest: {why}")
87 }
88 ManifestError::McidMismatch => {
89 f.write_str("the manifest does not describe the content id")
90 }
91 ManifestError::NotWhole(why) => write!(
92 f,
93 "the manifest's chunks do not describe its content whole: {why}"
94 ),
95 ManifestError::ChunkHashes => {
96 f.write_str("the manifest's chunk hashes do not make its root hash")
97 }
98 ManifestError::Malformed(why) => write!(f, "not a manifest: {why}"),
99 }
100 }
101}
102
103impl std::error::Error for ManifestError {}
104
105fn sha384(data: &[u8]) -> Hash {
106 Sha384::digest(data).into()
107}
108
109fn make_mcid(codec: u8, hash: &Hash) -> Mcid {
110 let mut out = [0u8; 50];
111 out[0] = TAG_SHA384;
112 out[1] = codec;
113 out[2..].copy_from_slice(hash);
114 out
115}
116
117pub fn create(
120 data: &[u8],
121 name: &str,
122 chunk_size: u64,
123) -> Result<(Manifest, Vec<Vec<u8>>), ManifestError> {
124 let now = std::time::SystemTime::now()
125 .duration_since(std::time::UNIX_EPOCH)
126 .map(|d| d.as_secs())
127 .unwrap_or(0);
128 create_at(data, name, chunk_size, now)
129}
130
131pub fn create_at(
133 data: &[u8],
134 name: &str,
135 chunk_size: u64,
136 created: u64,
137) -> Result<(Manifest, Vec<Vec<u8>>), ManifestError> {
138 if chunk_size == 0 {
139 return Err(ManifestError::ChunkSizeZero);
140 }
141 let chunks: Vec<Vec<u8>> = data
142 .chunks(chunk_size as usize)
143 .map(<[u8]>::to_vec)
144 .collect();
145 let infos = chunk_infos(&chunks);
146 let root_hash = root_hash_for(&infos);
147 let mut m = Manifest {
148 mcid: [0; 50],
149 version: VERSION,
150 name: name.as_bytes().to_vec(),
151 size: data.len() as u64,
152 created,
153 chunk_size,
154 chunk_count: infos.len() as u64,
155 hash_algorithm: SHA384.into(),
156 root_hash,
157 chunks: infos,
158 };
159 m.mcid = mcid_for(&m);
160 Ok((m, chunks))
161}
162
163pub fn chunk_mcid(m: &Manifest, index: usize) -> Option<Mcid> {
166 m.chunks.get(index).map(|c| make_mcid(CODEC_RAW, &c.hash))
167}
168
169pub fn block_mcid(data: &[u8]) -> Mcid {
171 make_mcid(CODEC_RAW, &sha384(data))
172}
173
174pub fn mcid_is_chunked(mcid: &Mcid) -> bool {
177 mcid[1] == CODEC_MANIFEST
178}
179
180pub fn mcid_for(m: &Manifest) -> Mcid {
184 let canonical = Value::Map(vec![
185 (
186 Value::text("name"),
187 Value::text(String::from_utf8_lossy(&m.name)),
188 ),
189 (Value::text("size"), Value::Int(i128::from(m.size))),
190 (
191 Value::text("chunk_size"),
192 Value::Int(i128::from(m.chunk_size)),
193 ),
194 (
195 Value::text("chunk_count"),
196 Value::Int(i128::from(m.chunk_count)),
197 ),
198 (
199 Value::text("hash_algorithm"),
200 Value::text(m.hash_algorithm.clone()),
201 ),
202 (Value::text("root_hash"), Value::Bytes(m.root_hash.to_vec())),
203 ]);
204 let encoded = cbor::encode(&canonical).expect("a manifest's canonical fields always encode");
205 make_mcid(CODEC_MANIFEST, &sha384(&encoded))
206}
207
208pub fn verify_mcid(m: &Manifest, mcid: &Mcid) -> Result<(), ManifestError> {
212 if std::str::from_utf8(&m.name).is_err() || m.hash_algorithm != SHA384 || mcid_for(m) != *mcid {
213 return Err(ManifestError::McidMismatch);
214 }
215 Ok(())
216}
217
218pub fn verify(m: &Manifest, data: &[u8]) -> Result<(), ManifestError> {
221 if m.chunk_size == 0 {
222 return Err(ManifestError::ChunkSizeZero);
223 }
224 if data.len() as u64 != m.size {
225 return Err(ManifestError::ContentMismatch(format!(
226 "{} bytes, the manifest's {}",
227 data.len(),
228 m.size
229 )));
230 }
231 let chunks: Vec<Vec<u8>> = data
232 .chunks(m.chunk_size as usize)
233 .map(<[u8]>::to_vec)
234 .collect();
235 if root_hash_for(&chunk_infos(&chunks)) != m.root_hash {
236 return Err(ManifestError::ContentMismatch("another root hash".into()));
237 }
238 Ok(())
239}
240
241pub fn check_whole(m: &Manifest) -> Result<(), ManifestError> {
246 let wanted = if m.chunk_size == 0 {
247 None
248 } else {
249 Some(m.size.div_ceil(m.chunk_size))
250 };
251 if wanted != Some(m.chunk_count) || m.chunk_count != m.chunks.len() as u64 {
252 return Err(ManifestError::NotWhole(format!(
253 "chunk size {}, size {}, {} chunks counted, {} listed",
254 m.chunk_size,
255 m.size,
256 m.chunk_count,
257 m.chunks.len()
258 )));
259 }
260 for (i, c) in m.chunks.iter().enumerate() {
261 let offset = i as u64 * m.chunk_size;
262 let size = m.chunk_size.min(m.size - offset);
263 if c.index != i as u64 || c.offset != offset || c.size != size {
264 return Err(ManifestError::NotWhole(format!("chunk {i}")));
265 }
266 }
267 Ok(())
268}
269
270pub fn check_chunk_hashes(m: &Manifest) -> Result<(), ManifestError> {
274 if root_hash_for(&m.chunks) != m.root_hash {
275 return Err(ManifestError::ChunkHashes);
276 }
277 Ok(())
278}
279
280fn chunk_infos(chunks: &[Vec<u8>]) -> Vec<ChunkInfo> {
281 let mut offset = 0u64;
282 chunks
283 .iter()
284 .enumerate()
285 .map(|(i, chunk)| {
286 let info = ChunkInfo {
287 index: i as u64,
288 offset,
289 size: chunk.len() as u64,
290 hash: sha384(chunk),
291 };
292 offset += chunk.len() as u64;
293 info
294 })
295 .collect()
296}
297
298fn root_hash_for(infos: &[ChunkInfo]) -> Hash {
302 let mut level: Vec<Hash> = infos.iter().map(|c| c.hash).collect();
303 if level.is_empty() {
304 return sha384(&[]);
305 }
306 while level.len() > 1 {
307 level = level
308 .chunks(2)
309 .map(|pair| {
310 let right = pair.get(1).unwrap_or(&pair[0]);
311 sha384(&[pair[0].as_slice(), right.as_slice()].concat())
312 })
313 .collect();
314 }
315 level[0]
316}
317
318pub fn to_wire(m: &Manifest) -> Value {
320 let chunks = m
321 .chunks
322 .iter()
323 .map(|c| {
324 Value::Map(vec![
325 (Value::text("index"), Value::Int(i128::from(c.index))),
326 (Value::text("offset"), Value::Int(i128::from(c.offset))),
327 (Value::text("size"), Value::Int(i128::from(c.size))),
328 (Value::text("hash"), Value::Bytes(c.hash.to_vec())),
329 ])
330 })
331 .collect();
332 Value::Map(vec![
333 (Value::text("mcid"), Value::Bytes(m.mcid.to_vec())),
334 (Value::text("version"), Value::Int(i128::from(m.version))),
335 (Value::text("name"), Value::Bytes(m.name.clone())),
336 (Value::text("size"), Value::Int(i128::from(m.size))),
337 (Value::text("created"), Value::Int(i128::from(m.created))),
338 (
339 Value::text("chunk_size"),
340 Value::Int(i128::from(m.chunk_size)),
341 ),
342 (
343 Value::text("chunk_count"),
344 Value::Int(i128::from(m.chunk_count)),
345 ),
346 (
347 Value::text("hash_algorithm"),
348 Value::text(m.hash_algorithm.clone()),
349 ),
350 (Value::text("root_hash"), Value::Bytes(m.root_hash.to_vec())),
351 (Value::text("chunks"), Value::List(chunks)),
352 ])
353}
354
355pub fn from_wire(v: &Value) -> Result<Manifest, ManifestError> {
361 let chunks = match v.get("chunks") {
362 Some(Value::List(items)) => items
363 .iter()
364 .map(chunk_from_wire)
365 .collect::<Result<Vec<_>, _>>()?,
366 _ => return Err(malformed("chunks")),
367 };
368 let m = Manifest {
369 mcid: bytes_exact(v, "mcid")?,
370 version: u32::try_from(uint(v, "version")?).map_err(|_| malformed("version"))?,
371 name: match v.get("name") {
372 Some(Value::Bytes(b)) => b.clone(),
373 _ => return Err(malformed("name")),
374 },
375 size: uint(v, "size")?,
376 created: uint(v, "created")?,
377 chunk_size: uint(v, "chunk_size")?,
378 chunk_count: uint(v, "chunk_count")?,
379 hash_algorithm: match v.get("hash_algorithm") {
380 Some(Value::Text(t)) if t == SHA384 => SHA384.into(),
381 Some(Value::Bytes(b)) if b == SHA384.as_bytes() => SHA384.into(),
382 _ => return Err(malformed("hash_algorithm")),
383 },
384 root_hash: bytes_exact(v, "root_hash")?,
385 chunks,
386 };
387 check_whole(&m)?;
388 Ok(m)
389}
390
391fn chunk_from_wire(v: &Value) -> Result<ChunkInfo, ManifestError> {
392 Ok(ChunkInfo {
393 index: uint(v, "index")?,
394 offset: uint(v, "offset")?,
395 size: uint(v, "size")?,
396 hash: bytes_exact(v, "hash")?,
397 })
398}
399
400fn malformed(field: &str) -> ManifestError {
401 ManifestError::Malformed(format!("field {field:?} is missing or of the wrong type"))
402}
403
404fn uint(v: &Value, field: &str) -> Result<u64, ManifestError> {
406 match v.get(field) {
407 Some(Value::Int(n)) if (0..=i128::from(i64::MAX)).contains(n) => Ok(*n as u64),
408 _ => Err(malformed(field)),
409 }
410}
411
412fn bytes_exact<const N: usize>(v: &Value, field: &str) -> Result<[u8; N], ManifestError> {
413 match v.get(field) {
414 Some(Value::Bytes(b)) => b.as_slice().try_into().map_err(|_| malformed(field)),
415 _ => Err(malformed(field)),
416 }
417}