1use anyhow::{anyhow, Result};
5use serde::{Deserialize, Serialize};
6
7pub const JUMBF_BOX_TYPE: [u8; 4] = *b"jumb";
9pub const DESCRIPTION_BOX_TYPE: [u8; 4] = *b"jumd";
10pub const JSON_BOX_TYPE: [u8; 4] = *b"json";
11pub const CBOR_BOX_TYPE: [u8; 4] = *b"cbor";
12pub const UUID_BOX_TYPE: [u8; 4] = *b"uuid";
13pub const EMBEDDED_FILE_BOX_TYPE: [u8; 4] = *b"bfdb";
14pub const CODESTREAM_BOX_TYPE: [u8; 4] = *b"jp2c";
15
16pub const C2PA_MANIFEST_UUID: [u8; 16] = [
18 0x63, 0x32, 0x70, 0x61, 0x00, 0x11, 0x00, 0x10, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
19];
20
21pub const C2PA_ASSERTION_STORE_UUID: [u8; 16] = [
23 0x63, 0x32, 0x61, 0x73, 0x00, 0x11, 0x00, 0x10, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
24];
25
26pub const C2PA_CLAIM_UUID: [u8; 16] = [
28 0x63, 0x32, 0x63, 0x6C, 0x00, 0x11, 0x00, 0x10, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
29];
30
31pub const C2PA_SIGNATURE_UUID: [u8; 16] = [
33 0x63, 0x32, 0x63, 0x73, 0x00, 0x11, 0x00, 0x10, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
34];
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct JumbfBox {
39 pub box_type: [u8; 4],
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub description: Option<DescriptionBox>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub content: Option<BoxContent>,
44 #[serde(skip_serializing_if = "Vec::is_empty")]
45 pub children: Vec<JumbfBox>,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
50pub struct DescriptionBox {
51 pub uuid: [u8; 16],
52 pub label: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub requestable: Option<bool>,
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize)]
59pub enum BoxContent {
60 Json(serde_json::Value),
61 Cbor(Vec<u8>),
62 Binary(Vec<u8>),
63}
64
65impl JumbfBox {
66 pub fn superbox(label: &str, uuid: [u8; 16]) -> Self {
67 Self {
68 box_type: JUMBF_BOX_TYPE,
69 description: Some(DescriptionBox {
70 uuid,
71 label: label.to_string(),
72 requestable: Some(true),
73 }),
74 content: None,
75 children: Vec::new(),
76 }
77 }
78
79 pub fn json_box(label: &str, content: serde_json::Value) -> Self {
80 Self {
81 box_type: JUMBF_BOX_TYPE,
82 description: Some(DescriptionBox {
83 uuid: [0; 16],
84 label: label.to_string(),
85 requestable: None,
86 }),
87 content: Some(BoxContent::Json(content)),
88 children: Vec::new(),
89 }
90 }
91
92 pub fn cbor_box(label: &str, content: Vec<u8>) -> Self {
93 Self {
94 box_type: JUMBF_BOX_TYPE,
95 description: Some(DescriptionBox {
96 uuid: [0; 16],
97 label: label.to_string(),
98 requestable: None,
99 }),
100 content: Some(BoxContent::Cbor(content)),
101 children: Vec::new(),
102 }
103 }
104
105 pub fn add_child(&mut self, child: JumbfBox) {
106 self.children.push(child);
107 }
108
109 pub fn find_by_label(&self, label: &str) -> Option<&JumbfBox> {
110 if self.description.as_ref().is_some_and(|d| d.label == label) {
111 return Some(self);
112 }
113 for child in &self.children {
114 if let Some(found) = child.find_by_label(label) {
115 return Some(found);
116 }
117 }
118 None
119 }
120
121 pub fn encode(&self) -> Result<Vec<u8>> {
123 let mut buf = Vec::new();
124 encode_box(self, &mut buf)?;
125 Ok(buf)
126 }
127
128 pub fn decode(data: &[u8]) -> Result<Self> {
130 let (jbox, consumed) = decode_box(data)?;
131 if consumed != data.len() {
132 return Err(anyhow!(
133 "trailing data: consumed {consumed}, total {}",
134 data.len()
135 ));
136 }
137 Ok(jbox)
138 }
139}
140
141fn encode_box(jbox: &JumbfBox, buf: &mut Vec<u8>) -> Result<()> {
142 let start = buf.len();
143 buf.extend_from_slice(&[0u8; 4]);
144 buf.extend_from_slice(&jbox.box_type);
145
146 if let Some(desc) = &jbox.description {
147 encode_description_box(desc, buf)?;
148 }
149
150 match &jbox.content {
151 Some(BoxContent::Json(val)) => {
152 let json_bytes =
153 serde_json::to_vec(val).map_err(|e| anyhow!("JSON serialization failed: {e}"))?;
154 let content_len = (8 + json_bytes.len()) as u32;
155 buf.extend_from_slice(&content_len.to_be_bytes());
156 buf.extend_from_slice(&JSON_BOX_TYPE);
157 buf.extend_from_slice(&json_bytes);
158 }
159 Some(BoxContent::Cbor(data)) => {
160 let content_len = (8 + data.len()) as u32;
161 buf.extend_from_slice(&content_len.to_be_bytes());
162 buf.extend_from_slice(&CBOR_BOX_TYPE);
163 buf.extend_from_slice(data);
164 }
165 Some(BoxContent::Binary(data)) => {
166 let content_len = (8 + data.len()) as u32;
167 buf.extend_from_slice(&content_len.to_be_bytes());
168 buf.extend_from_slice(&EMBEDDED_FILE_BOX_TYPE);
169 buf.extend_from_slice(data);
170 }
171 None => {}
172 }
173
174 for child in &jbox.children {
175 encode_box(child, buf)?;
176 }
177
178 let total_len = (buf.len() - start) as u32;
179 buf[start..start + 4].copy_from_slice(&total_len.to_be_bytes());
180
181 Ok(())
182}
183
184fn encode_description_box(desc: &DescriptionBox, buf: &mut Vec<u8>) -> Result<()> {
185 let start = buf.len();
186 buf.extend_from_slice(&[0u8; 4]);
187 buf.extend_from_slice(&DESCRIPTION_BOX_TYPE);
188 buf.extend_from_slice(&desc.uuid);
189
190 let toggle = if desc.requestable.unwrap_or(false) {
191 0x03
192 } else {
193 0x00
194 };
195 buf.push(toggle);
196
197 buf.extend_from_slice(desc.label.as_bytes());
198 buf.push(0);
199
200 let total_len = (buf.len() - start) as u32;
201 buf[start..start + 4].copy_from_slice(&total_len.to_be_bytes());
202
203 Ok(())
204}
205
206const MAX_JUMBF_DEPTH: usize = 64;
209
210fn decode_box(data: &[u8]) -> Result<(JumbfBox, usize)> {
211 decode_box_depth(data, 0)
212}
213
214fn decode_box_depth(data: &[u8], depth: usize) -> Result<(JumbfBox, usize)> {
215 if depth > MAX_JUMBF_DEPTH {
216 return Err(anyhow!(
217 "JUMBF nesting exceeds maximum depth of {MAX_JUMBF_DEPTH}"
218 ));
219 }
220 if data.len() < 8 {
221 return Err(anyhow!("insufficient data for box header"));
222 }
223 let box_len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
224 if box_len < 8 || box_len > data.len() {
225 return Err(anyhow!("invalid box length: {box_len}"));
226 }
227 let box_type: [u8; 4] = [data[4], data[5], data[6], data[7]];
228 let box_data = &data[8..box_len];
229
230 let mut offset = 0;
231 let mut description = None;
232 let mut content = None;
233 let mut children = Vec::new();
234
235 while offset < box_data.len() {
236 if box_data.len() - offset < 8 {
237 break;
238 }
239 let inner_len = u32::from_be_bytes([
240 box_data[offset],
241 box_data[offset + 1],
242 box_data[offset + 2],
243 box_data[offset + 3],
244 ]) as usize;
245 if inner_len < 8 || offset + inner_len > box_data.len() {
246 break;
247 }
248 let inner_type: [u8; 4] = [
249 box_data[offset + 4],
250 box_data[offset + 5],
251 box_data[offset + 6],
252 box_data[offset + 7],
253 ];
254 let inner_data = &box_data[offset + 8..offset + inner_len];
255
256 match inner_type {
257 DESCRIPTION_BOX_TYPE => {
258 description = Some(decode_description_box(inner_data)?);
259 }
260 JSON_BOX_TYPE => {
261 let val = serde_json::from_slice(inner_data)
262 .map_err(|e| anyhow!("JSON decode failed: {e}"))?;
263 content = Some(BoxContent::Json(val));
264 }
265 CBOR_BOX_TYPE => {
266 content = Some(BoxContent::Cbor(inner_data.to_vec()));
267 }
268 JUMBF_BOX_TYPE => {
269 let (child, _) = decode_box_depth(&box_data[offset..], depth + 1)?;
270 children.push(child);
271 }
272 _ => {
273 content = Some(BoxContent::Binary(inner_data.to_vec()));
274 }
275 }
276
277 offset += inner_len;
278 }
279
280 Ok((
281 JumbfBox {
282 box_type,
283 description,
284 content,
285 children,
286 },
287 box_len,
288 ))
289}
290
291fn decode_description_box(data: &[u8]) -> Result<DescriptionBox> {
292 if data.len() < 17 {
293 return Err(anyhow!("description box too short"));
294 }
295 let mut uuid = [0u8; 16];
296 uuid.copy_from_slice(&data[..16]);
297 let toggle = data[16];
298 let requestable = Some(toggle & 0x01 != 0);
299
300 let label_data = &data[17..];
301 let label_end = label_data
302 .iter()
303 .position(|&b| b == 0)
304 .unwrap_or(label_data.len());
305 let label = String::from_utf8(label_data[..label_end].to_vec())
306 .map_err(|e| anyhow!("invalid label UTF-8: {e}"))?;
307
308 Ok(DescriptionBox {
309 uuid,
310 label,
311 requestable,
312 })
313}
314
315pub fn build_c2pa_manifest_box(
317 label: &str,
318 claim_json: serde_json::Value,
319 signature_cbor: Vec<u8>,
320 assertions: Vec<(&str, serde_json::Value)>,
321) -> JumbfBox {
322 let mut manifest = JumbfBox::superbox(label, C2PA_MANIFEST_UUID);
323
324 let mut assertion_store = JumbfBox::superbox("c2pa.assertions", C2PA_ASSERTION_STORE_UUID);
325 for (assertion_label, assertion_value) in assertions {
326 assertion_store.add_child(JumbfBox::json_box(assertion_label, assertion_value));
327 }
328 manifest.add_child(assertion_store);
329 manifest.add_child(JumbfBox::json_box("c2pa.claim", claim_json));
330 manifest.add_child(JumbfBox::cbor_box("c2pa.signature", signature_cbor));
331
332 manifest
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn superbox_with_children() {
341 let mut parent = JumbfBox::superbox("test-manifest", C2PA_MANIFEST_UUID);
342 parent.add_child(JumbfBox::json_box(
343 "assertion",
344 serde_json::json!({"type": "test"}),
345 ));
346 assert_eq!(parent.children.len(), 1);
347 assert!(parent.find_by_label("assertion").is_some());
348 assert!(parent.find_by_label("nonexistent").is_none());
349 }
350
351 #[test]
352 fn encode_decode_roundtrip() {
353 let mut manifest = JumbfBox::superbox("test-manifest", C2PA_MANIFEST_UUID);
354 manifest.add_child(JumbfBox::json_box(
355 "c2pa.claim",
356 serde_json::json!({"title": "test"}),
357 ));
358 manifest.add_child(JumbfBox::cbor_box("c2pa.signature", vec![0xA0]));
359
360 let encoded = manifest.encode().unwrap();
361 let decoded = JumbfBox::decode(&encoded).unwrap();
362
363 assert_eq!(decoded.box_type, JUMBF_BOX_TYPE);
364 let desc = decoded.description.as_ref().unwrap();
365 assert_eq!(desc.label, "test-manifest");
366 assert_eq!(desc.uuid, C2PA_MANIFEST_UUID);
367 }
368
369 #[test]
370 fn c2pa_manifest_box_structure() {
371 let claim = serde_json::json!({
372 "dc:title": "HMS Knowledge Store",
373 "claim_generator": "HMS/0.1"
374 });
375 let signature = vec![0xD2, 0x84, 0x43];
376 let assertions = vec![
377 (
378 "c2pa.hash.data",
379 serde_json::json!({"alg": "sha256", "hash": "abc123"}),
380 ),
381 (
382 "c2pa.actions",
383 serde_json::json!({"actions": [{"action": "c2pa.created"}]}),
384 ),
385 ];
386
387 let manifest = build_c2pa_manifest_box("urn:hms:manifest:1", claim, signature, assertions);
388
389 assert!(manifest.find_by_label("c2pa.assertions").is_some());
390 assert!(manifest.find_by_label("c2pa.claim").is_some());
391 assert!(manifest.find_by_label("c2pa.signature").is_some());
392 assert!(manifest.find_by_label("c2pa.hash.data").is_some());
393 assert!(manifest.find_by_label("c2pa.actions").is_some());
394 }
395
396 #[test]
397 fn binary_encode_valid_structure() {
398 let jbox = JumbfBox::json_box("test", serde_json::json!({"k": "v"}));
399 let mut parent = JumbfBox::superbox("parent", [0; 16]);
400 parent.add_child(jbox);
401
402 let encoded = parent.encode().unwrap();
403 assert!(encoded.len() > 16);
404 assert_eq!(&encoded[4..8], &JUMBF_BOX_TYPE);
405
406 let decoded = JumbfBox::decode(&encoded).unwrap();
407 assert!(decoded.find_by_label("test").is_some());
408 }
409
410 #[test]
411 fn nested_encode_decode() {
412 let claim = serde_json::json!({"title": "roundtrip test"});
413 let manifest = build_c2pa_manifest_box("urn:test:1", claim, vec![0xA0, 0xB1], vec![]);
414
415 let encoded = manifest.encode().unwrap();
416 let decoded = JumbfBox::decode(&encoded).unwrap();
417
418 let found_claim = decoded.find_by_label("c2pa.claim").unwrap();
419 match &found_claim.content {
420 Some(BoxContent::Json(v)) => assert_eq!(v["title"], "roundtrip test"),
421 _ => panic!("expected JSON content in claim box"),
422 }
423 }
424}