1use crate::blob::HEADER_SIZE;
6use crate::blob::error::BlobError;
7use crate::blob::frame::{FrameError, decode_exact};
8use crate::blob::kind::BlobKind;
9
10pub fn parse_cnb<K: BlobKind>(validity: u32, data: &[u8]) -> Result<(K, usize), BlobError> {
22 let meta_len = parse_header::<K>(data)? as usize;
23
24 let stored = le_u32(data, 4).ok_or(BlobError::TooShort)?;
25 if stored != validity {
26 return Err(BlobError::ValidityMismatch(stored));
27 }
28
29 let meta_end = HEADER_SIZE
30 .checked_add(meta_len)
31 .ok_or(BlobError::TruncatedMeta)?;
32 let meta_bytes = data
33 .get(HEADER_SIZE..meta_end)
34 .ok_or(BlobError::TruncatedMeta)?;
35 let meta = decode_exact(meta_bytes).map_err(|e| match e {
36 FrameError::Decode(_) => BlobError::Decode,
37 FrameError::Trailing(n) => BlobError::TrailingMeta(n),
38 })?;
39 Ok((meta, meta_end))
40}
41
42pub fn parse_payload_section_start<K: BlobKind>(header: &[u8]) -> Result<u64, BlobError> {
46 Ok(HEADER_SIZE as u64 + parse_header::<K>(header)?)
47}
48
49pub fn payload_section(data: &[u8]) -> &[u8] {
56 let Some(meta_len) = le_u64(data, 8) else {
57 return &[];
58 };
59 let meta_len = meta_len as usize;
60 HEADER_SIZE
61 .checked_add(meta_len)
62 .and_then(|start| data.get(start..))
63 .unwrap_or(&[])
64}
65
66fn parse_header<K: BlobKind>(data: &[u8]) -> Result<u64, BlobError> {
69 if data.len() < HEADER_SIZE {
70 return Err(BlobError::TooShort);
71 }
72 if data.get(..4) != Some(&K::MAGIC[..]) {
73 return Err(BlobError::BadMagic);
74 }
75 le_u64(data, 8).ok_or(BlobError::TooShort)
76}
77
78fn le_u32(data: &[u8], at: usize) -> Option<u32> {
80 data.get(at..)?
81 .first_chunk::<4>()
82 .copied()
83 .map(u32::from_le_bytes)
84}
85
86fn le_u64(data: &[u8], at: usize) -> Option<u64> {
88 data.get(at..)?
89 .first_chunk::<8>()
90 .copied()
91 .map(u64::from_le_bytes)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::blob::cache::CacheMeta;
98 use crate::blob::encode::encode_cnb;
99 use crate::blob::schema::{AssetKind, BlobAssetDef, BlobMeta, ResourceKind, ResourceRecord};
100 use crate::blob::{BLOB_MAGIC, kind::BlobKind};
101 use crate::ecs::PayloadLocator;
102 use alloc::vec;
103 use alloc::vec::Vec;
104
105 const TEST_SCHEMA_VERSION: u32 = 0x1234_5678;
108
109 fn def(discriminant: u8, args_bytes: Vec<u8>) -> BlobAssetDef {
110 BlobAssetDef {
111 name: None,
112 kind: AssetKind::Component,
113 discriminant,
114 args_bytes,
115 payload: None,
116 }
117 }
118
119 fn meta() -> BlobMeta {
120 let defs = vec![def(3, vec![1, 2]), def(9, vec![])];
121 let resources = vec![ResourceRecord {
122 resource_kind: ResourceKind::AudioClip as u8,
123 handle: 0,
124 payload: Some(PayloadLocator {
125 blob_index: 0,
126 offset: 0,
127 len: 3,
128 }),
129 data_bytes: Vec::new(),
130 }];
131 let manifest = crate::blob::WorldManifest::from_records(&defs, &resources);
132 BlobMeta {
133 defs,
134 resources,
135 manifest,
136 scene_groups: Vec::new(),
137 mesh_bounds: Vec::new(),
138 physics_budget: None,
139 }
140 }
141
142 #[test]
143 fn encode_round_trips_defs_resources_and_payload() {
144 let m = meta();
145 let payload = [0xAA, 0xBB, 0xCC];
146 let image = encode_cnb(TEST_SCHEMA_VERSION, &m, &payload).unwrap();
147
148 let (got, payload_start) =
149 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &image).expect("parse");
150 assert_eq!(got, m);
151 assert_eq!(&image[payload_start..], &payload);
152 assert_eq!(
153 parse_payload_section_start::<BlobMeta>(&image).unwrap(),
154 payload_start as u64
155 );
156 assert_eq!(payload_section(&image), &payload);
157 }
158
159 #[test]
160 fn encode_with_no_metadata_and_no_payload_is_parseable() {
161 let image = encode_cnb(TEST_SCHEMA_VERSION, &BlobMeta::default(), &[]).unwrap();
162 let (m, payload_start) = parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &image).expect("parse");
163 assert!(m.defs.is_empty());
164 assert!(m.resources.is_empty());
165 assert_eq!(image.len(), payload_start);
166 assert_eq!(payload_section(&image), &[] as &[u8]);
167 }
168
169 #[test]
170 fn encode_emits_magic_and_validity_token_header() {
171 let image = encode_cnb(TEST_SCHEMA_VERSION, &BlobMeta::default(), &[1]).unwrap();
172 assert_eq!(&image[0..4], &BLOB_MAGIC);
173 assert_eq!(
174 u32::from_le_bytes(image[4..8].try_into().unwrap()),
175 TEST_SCHEMA_VERSION
176 );
177 let meta_len = u64::from_le_bytes(image[8..16].try_into().unwrap()) as usize;
178 assert_eq!(image.len(), HEADER_SIZE + meta_len + 1);
179 }
180
181 #[test]
185 fn a_container_of_another_kind_is_rejected() {
186 let other = encode_cnb(TEST_SCHEMA_VERSION, &CacheMeta::default(), &[]).unwrap();
187 assert_eq!(&other[0..4], &CacheMeta::MAGIC);
188 assert_eq!(
189 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &other),
190 Err(BlobError::BadMagic)
191 );
192 assert_eq!(
193 parse_payload_section_start::<BlobMeta>(&other),
194 Err(BlobError::BadMagic)
195 );
196
197 let world = encode_cnb(TEST_SCHEMA_VERSION, &meta(), &[]).unwrap();
198 assert_eq!(
199 parse_cnb::<CacheMeta>(TEST_SCHEMA_VERSION, &world),
200 Err(BlobError::BadMagic)
201 );
202 }
203
204 #[test]
205 fn parse_rejects_short_bad_magic_and_validity_mismatch() {
206 assert_eq!(
207 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &[0u8; HEADER_SIZE - 1]),
208 Err(BlobError::TooShort)
209 );
210 assert_eq!(
211 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &[0u8; HEADER_SIZE]),
212 Err(BlobError::BadMagic)
213 );
214
215 let mut mismatched = encode_cnb(TEST_SCHEMA_VERSION, &BlobMeta::default(), &[]).unwrap();
216 let stored = TEST_SCHEMA_VERSION.wrapping_add(1);
217 mismatched[4..8].copy_from_slice(&stored.to_le_bytes());
218 assert_eq!(
219 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &mismatched),
220 Err(BlobError::ValidityMismatch(stored))
221 );
222 }
223
224 #[test]
228 fn parse_rejects_a_meta_section_with_unread_bytes() {
229 let image = encode_cnb(TEST_SCHEMA_VERSION, &meta(), &[]).unwrap();
230 let meta_len = u64::from_le_bytes(image[8..16].try_into().unwrap());
231
232 let mut widened = image.clone();
233 widened[8..16].copy_from_slice(&(meta_len + 2).to_le_bytes());
234 widened.extend_from_slice(&[0, 0]);
235
236 assert_eq!(
237 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &widened),
238 Err(BlobError::TrailingMeta(2))
239 );
240 }
241
242 #[test]
243 fn parse_rejects_a_truncated_meta_section() {
244 let image = encode_cnb(TEST_SCHEMA_VERSION, &meta(), &[]).unwrap();
245 assert_eq!(
246 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, &image[..image.len() - 1]),
247 Err(BlobError::TruncatedMeta)
248 );
249 }
250
251 #[test]
252 fn parse_rejects_a_non_blob_image() {
253 let garbage = b"this is not a blob file at all";
254 assert_eq!(
255 parse_cnb::<BlobMeta>(TEST_SCHEMA_VERSION, garbage),
256 Err(BlobError::BadMagic)
257 );
258 assert_eq!(
259 parse_payload_section_start::<BlobMeta>(garbage),
260 Err(BlobError::BadMagic)
261 );
262 }
263
264 #[test]
265 fn payload_section_is_empty_for_a_headerless_or_overrun_image() {
266 assert_eq!(payload_section(&[]), &[] as &[u8]);
267 assert_eq!(payload_section(&[0u8; HEADER_SIZE - 1]), &[] as &[u8]);
268
269 let mut overrun = [0u8; HEADER_SIZE];
271 overrun[0..4].copy_from_slice(&BLOB_MAGIC);
272 overrun[8..16].copy_from_slice(&u64::MAX.to_le_bytes());
273 assert_eq!(payload_section(&overrun), &[] as &[u8]);
274 }
275
276 #[test]
279 fn payload_section_ignores_magic() {
280 let mut image = encode_cnb(TEST_SCHEMA_VERSION, &BlobMeta::default(), b"overflow").unwrap();
281 image[0..4].copy_from_slice(b"XXXX");
282 assert_eq!(payload_section(&image), b"overflow");
283 }
284}