khora_io/asset/
manifest.rs1use anyhow::{anyhow, Context, Result};
22use khora_core::asset::AssetUUID;
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ManifestEntry {
29 pub uuid: AssetUUID,
30 pub blake3: [u8; 32],
32 pub size: u64,
35}
36
37#[derive(Debug, Default, Clone)]
39pub struct PackManifest {
40 by_uuid: HashMap<AssetUUID, ManifestEntry>,
41}
42
43impl PackManifest {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn insert(&mut self, uuid: AssetUUID, bytes: &[u8]) {
51 let hash: [u8; 32] = *blake3::hash(bytes).as_bytes();
52 self.by_uuid.insert(
53 uuid,
54 ManifestEntry {
55 uuid,
56 blake3: hash,
57 size: bytes.len() as u64,
58 },
59 );
60 }
61
62 pub fn encode(&self) -> Result<Vec<u8>> {
64 let mut entries: Vec<ManifestEntry> = self.by_uuid.values().cloned().collect();
65 entries.sort_by_key(|a| a.blake3);
70 let cfg = bincode::config::standard();
71 bincode::serde::encode_to_vec(&entries, cfg)
72 .map_err(|e| anyhow!("Failed to encode manifest: {}", e))
73 }
74
75 pub fn decode(bytes: &[u8]) -> Result<Self> {
77 let cfg = bincode::config::standard();
78 let (entries, _): (Vec<ManifestEntry>, _) = bincode::serde::decode_from_slice(bytes, cfg)
79 .context("Failed to decode manifest.bin")?;
80 let by_uuid = entries.into_iter().map(|e| (e.uuid, e)).collect();
81 Ok(Self { by_uuid })
82 }
83
84 pub fn is_empty(&self) -> bool {
86 self.by_uuid.is_empty()
87 }
88
89 pub fn len(&self) -> usize {
91 self.by_uuid.len()
92 }
93
94 pub fn get(&self, uuid: &AssetUUID) -> Option<&ManifestEntry> {
96 self.by_uuid.get(uuid)
97 }
98
99 pub fn verify(&self, uuid: &AssetUUID, bytes: &[u8]) -> Result<()> {
104 let Some(entry) = self.by_uuid.get(uuid) else {
105 return Err(anyhow!("Manifest: no record for asset {:?}", uuid));
106 };
107 if entry.size != bytes.len() as u64 {
108 return Err(anyhow!(
109 "Manifest: size mismatch for {:?} (expected {}, got {})",
110 uuid,
111 entry.size,
112 bytes.len()
113 ));
114 }
115 let actual: [u8; 32] = *blake3::hash(bytes).as_bytes();
116 if actual != entry.blake3 {
117 return Err(anyhow!(
118 "Manifest: BLAKE3 mismatch for {:?} (corrupted or tampered)",
119 uuid
120 ));
121 }
122 Ok(())
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn round_trip_encode_decode() {
132 let mut m = PackManifest::new();
133 m.insert(AssetUUID::new_v5("a.png"), b"AAAA");
134 m.insert(AssetUUID::new_v5("b.png"), b"BBBB");
135 let bytes = m.encode().unwrap();
136 let m2 = PackManifest::decode(&bytes).unwrap();
137 assert_eq!(m.len(), m2.len());
138 for (uuid, entry) in &m.by_uuid {
139 let other = m2.get(uuid).unwrap();
140 assert_eq!(other.blake3, entry.blake3);
141 assert_eq!(other.size, entry.size);
142 }
143 }
144
145 #[test]
146 fn verify_accepts_correct_bytes_and_rejects_corruption() {
147 let mut m = PackManifest::new();
148 let uuid = AssetUUID::new_v5("foo");
149 m.insert(uuid, b"PAYLOAD");
150 m.verify(&uuid, b"PAYLOAD").unwrap();
151 assert!(m.verify(&uuid, b"DIFFERENT").is_err());
152 assert!(m.verify(&uuid, b"PAYLOAD-EXTRA").is_err()); }
154
155 #[test]
156 fn verify_unknown_uuid_errors() {
157 let m = PackManifest::new();
158 let unknown = AssetUUID::new_v5("nope");
159 assert!(m.verify(&unknown, b"x").is_err());
160 }
161}