1use serde::{Deserialize, Serialize};
24use std::collections::{BTreeMap, HashMap};
25
26use crate::hash::sha256;
27use crate::types::{Hash, Link, LinkType, TreeNode};
28
29#[derive(Debug, thiserror::Error)]
31pub enum CodecError {
32 #[error("Invalid node type: {0}")]
33 InvalidNodeType(u8),
34 #[error("Invalid link type: {0}")]
35 InvalidLinkType(u8),
36 #[error("Missing required field: {0}")]
37 MissingField(&'static str),
38 #[error("Invalid field type for {0}")]
39 InvalidFieldType(&'static str),
40 #[error("MessagePack encoding error: {0}")]
41 MsgpackEncode(String),
42 #[error("MessagePack decoding error: {0}")]
43 MsgpackDecode(String),
44 #[error("Invalid hash length: expected 32, got {0}")]
45 InvalidHashLength(usize),
46}
47
48#[derive(Serialize, Deserialize)]
51struct WireLink {
52 #[serde(with = "serde_bytes")]
54 h: Vec<u8>,
55 #[serde(
57 default,
58 skip_serializing_if = "Option::is_none",
59 with = "option_bytes"
60 )]
61 k: Option<Vec<u8>>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 m: Option<BTreeMap<String, serde_json::Value>>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 n: Option<String>,
68 s: u64,
70 #[serde(default)]
72 t: u8,
73}
74
75mod option_bytes {
77 use serde::{Deserialize, Deserializer, Serializer};
78
79 pub fn serialize<S>(data: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
80 where
81 S: Serializer,
82 {
83 match data {
84 Some(bytes) => serde_bytes::serialize(bytes, serializer),
85 None => serializer.serialize_none(),
86 }
87 }
88
89 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
90 where
91 D: Deserializer<'de>,
92 {
93 Option::<serde_bytes::ByteBuf>::deserialize(deserializer)
94 .map(|opt| opt.map(|bb| bb.into_vec()))
95 }
96}
97
98#[derive(Serialize, Deserialize)]
101struct WireTreeNode {
102 l: Vec<WireLink>,
104 t: u8,
106}
107
108#[derive(Deserialize)]
110#[allow(dead_code)]
111struct WireLinkShape {
112 #[serde(with = "serde_bytes")]
113 h: Vec<u8>,
114 #[serde(default)]
115 t: u8,
116}
117
118#[derive(Deserialize)]
119#[allow(dead_code)]
120struct WireTreeShape {
121 l: Vec<WireLinkShape>,
122 t: u8,
123}
124
125pub fn encode_tree_node(node: &TreeNode) -> Result<Vec<u8>, CodecError> {
127 let links = links_for_encoding(node);
128 let wire = WireTreeNode {
129 t: node.node_type as u8,
130 l: links
131 .iter()
132 .map(|link| {
133 let sorted_meta = link
135 .meta
136 .as_ref()
137 .map(|m| m.iter().collect::<BTreeMap<_, _>>());
138 WireLink {
139 h: link.hash.to_vec(),
140 t: link.link_type as u8,
141 n: link.name.clone(),
142 s: link.size,
143 k: link.key.map(|k| k.to_vec()),
144 m: sorted_meta
145 .map(|m| m.into_iter().map(|(k, v)| (k.clone(), v.clone())).collect()),
146 }
147 })
148 .collect(),
149 };
150
151 rmp_serde::to_vec_named(&wire).map_err(|e| CodecError::MsgpackEncode(e.to_string()))
152}
153
154fn links_for_encoding(node: &TreeNode) -> Vec<&Link> {
155 let mut links = node.links.iter().collect::<Vec<_>>();
156 if node.node_type == LinkType::Dir {
157 links.sort_by(|left, right| dir_link_sort_name(left).cmp(dir_link_sort_name(right)));
158 }
159 links
160}
161
162fn dir_link_sort_name(link: &Link) -> &str {
163 link.name.as_deref().unwrap_or("")
164}
165
166pub fn decode_tree_node(data: &[u8]) -> Result<TreeNode, CodecError> {
168 let wire: WireTreeNode =
169 rmp_serde::from_slice(data).map_err(|e| CodecError::MsgpackDecode(e.to_string()))?;
170
171 let node_type = LinkType::from_u8(wire.t)
173 .filter(|t| t.is_tree())
174 .ok_or(CodecError::InvalidNodeType(wire.t))?;
175
176 let mut links = Vec::with_capacity(wire.l.len());
177 for wl in wire.l {
178 if wl.h.len() != 32 {
179 return Err(CodecError::InvalidHashLength(wl.h.len()));
180 }
181 let mut hash = [0u8; 32];
182 hash.copy_from_slice(&wl.h);
183
184 let key = match wl.k {
185 Some(k) if k.len() == 32 => {
186 let mut key = [0u8; 32];
187 key.copy_from_slice(&k);
188 Some(key)
189 }
190 _ => None,
191 };
192
193 let link_type = LinkType::from_u8(wl.t).ok_or(CodecError::InvalidLinkType(wl.t))?;
194
195 let meta = wl.m.map(|m| m.into_iter().collect::<HashMap<_, _>>());
197
198 links.push(Link {
199 hash,
200 name: wl.n,
201 size: wl.s,
202 key,
203 link_type,
204 meta,
205 });
206 }
207
208 Ok(TreeNode { node_type, links })
209}
210
211pub fn encode_and_hash(node: &TreeNode) -> Result<(Vec<u8>, Hash), CodecError> {
213 let data = encode_tree_node(node)?;
214 let hash = sha256(&data);
215 Ok((data, hash))
216}
217
218pub fn try_decode_tree_node(data: &[u8]) -> Option<TreeNode> {
222 decode_tree_node(data).ok()
223}
224
225pub fn get_node_type(data: &[u8]) -> LinkType {
228 try_decode_tree_node(data)
229 .map(|n| n.node_type)
230 .unwrap_or(LinkType::Blob)
231}
232
233pub fn is_tree_node(data: &[u8]) -> bool {
239 if try_decode_tree_node(data).is_some() {
240 return true;
241 }
242
243 rmp_serde::from_slice::<WireTreeShape>(data).is_ok()
244}
245
246pub fn is_directory_node(data: &[u8]) -> bool {
249 try_decode_tree_node(data)
250 .map(|n| n.node_type.is_directory_like())
251 .unwrap_or(false)
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::types::to_hex;
258
259 #[test]
260 fn test_encode_decode_empty_tree() {
261 let node = TreeNode::dir(vec![]);
262
263 let encoded = encode_tree_node(&node).unwrap();
264 let decoded = decode_tree_node(&encoded).unwrap();
265
266 assert_eq!(decoded.links.len(), 0);
267 assert_eq!(decoded.node_type, LinkType::Dir);
268 }
269
270 #[test]
271 fn test_encode_decode_tree_with_links() {
272 let hash1 = [1u8; 32];
273 let hash2 = [2u8; 32];
274
275 let node = TreeNode::dir(vec![
276 Link {
277 hash: hash1,
278 name: Some("file1.txt".to_string()),
279 size: 100,
280 key: None,
281 link_type: LinkType::Blob,
282 meta: None,
283 },
284 Link {
285 hash: hash2,
286 name: Some("dir".to_string()),
287 size: 0,
288 key: None,
289 link_type: LinkType::Dir,
290 meta: None,
291 },
292 ]);
293
294 let encoded = encode_tree_node(&node).unwrap();
295 let decoded = decode_tree_node(&encoded).unwrap();
296
297 assert_eq!(decoded.links.len(), 2);
298 assert_eq!(decoded.links[0].name, Some("dir".to_string()));
299 assert_eq!(decoded.links[0].size, 0);
300 assert_eq!(decoded.links[0].link_type, LinkType::Dir);
301 assert_eq!(to_hex(&decoded.links[0].hash), to_hex(&hash2));
302 assert_eq!(decoded.links[1].name, Some("file1.txt".to_string()));
303 assert_eq!(decoded.links[1].size, 100);
304 assert_eq!(decoded.links[1].link_type, LinkType::Blob);
305 assert_eq!(to_hex(&decoded.links[1].hash), to_hex(&hash1));
306 }
307
308 #[test]
309 fn test_preserve_link_meta() {
310 let mut meta = HashMap::new();
311 meta.insert("createdAt".to_string(), serde_json::json!(1234567890));
312 meta.insert("mimeType".to_string(), serde_json::json!("image/png"));
313
314 let node = TreeNode::dir(vec![Link::new([1u8; 32])
315 .with_name("file.png")
316 .with_size(1024)
317 .with_meta(meta.clone())]);
318
319 let encoded = encode_tree_node(&node).unwrap();
320 let decoded = decode_tree_node(&encoded).unwrap();
321
322 assert!(decoded.links[0].meta.is_some());
323 let m = decoded.links[0].meta.as_ref().unwrap();
324 assert_eq!(m.get("createdAt"), Some(&serde_json::json!(1234567890)));
325 assert_eq!(m.get("mimeType"), Some(&serde_json::json!("image/png")));
326 }
327
328 #[test]
329 fn test_links_without_optional_fields() {
330 let hash = [42u8; 32];
331
332 let node = TreeNode::file(vec![Link::new(hash)]);
333
334 let encoded = encode_tree_node(&node).unwrap();
335 let decoded = decode_tree_node(&encoded).unwrap();
336
337 assert_eq!(decoded.links[0].name, None);
338 assert_eq!(decoded.links[0].size, 0);
339 assert_eq!(decoded.links[0].link_type, LinkType::Blob);
340 assert_eq!(decoded.links[0].meta, None);
341 assert_eq!(to_hex(&decoded.links[0].hash), to_hex(&hash));
342 }
343
344 #[test]
345 fn test_encode_and_hash() {
346 let node = TreeNode::dir(vec![]);
347
348 let (data, hash) = encode_and_hash(&node).unwrap();
349 let expected_hash = sha256(&data);
350
351 assert_eq!(to_hex(&hash), to_hex(&expected_hash));
352 }
353
354 #[test]
355 fn test_encode_and_hash_consistent() {
356 let node = TreeNode::dir(vec![Link {
357 hash: [1u8; 32],
358 name: Some("test".to_string()),
359 size: 100,
360 key: None,
361 link_type: LinkType::Blob,
362 meta: None,
363 }]);
364
365 let (_, hash1) = encode_and_hash(&node).unwrap();
366 let (_, hash2) = encode_and_hash(&node).unwrap();
367
368 assert_eq!(to_hex(&hash1), to_hex(&hash2));
369 }
370
371 #[test]
372 fn test_is_tree_node() {
373 let node = TreeNode::dir(vec![]);
374 let encoded = encode_tree_node(&node).unwrap();
375
376 assert!(is_tree_node(&encoded));
377 }
378
379 #[test]
380 fn test_is_tree_node_raw_blob() {
381 let blob = vec![1u8, 2, 3, 4, 5];
382 assert!(!is_tree_node(&blob));
383 }
384
385 #[test]
386 fn test_is_tree_node_invalid_msgpack() {
387 let invalid = vec![255u8, 255, 255];
388 assert!(!is_tree_node(&invalid));
389 }
390
391 #[test]
392 fn test_is_tree_node_unknown_node_type() {
393 let bytes = rmp_serde::to_vec_named(&WireTreeNode { l: vec![], t: 99 }).unwrap();
394
395 assert!(is_tree_node(&bytes));
396 assert!(matches!(
397 decode_tree_node(&bytes),
398 Err(CodecError::InvalidNodeType(99))
399 ));
400 }
401
402 #[test]
403 fn test_is_tree_node_unknown_link_type() {
404 let bytes = rmp_serde::to_vec_named(&WireTreeNode {
405 l: vec![WireLink {
406 h: vec![1u8; 32],
407 k: None,
408 m: None,
409 n: None,
410 s: 1,
411 t: 99,
412 }],
413 t: LinkType::Dir as u8,
414 })
415 .unwrap();
416
417 assert!(is_tree_node(&bytes));
418 assert!(matches!(
419 decode_tree_node(&bytes),
420 Err(CodecError::InvalidLinkType(99))
421 ));
422 }
423
424 #[test]
425 fn test_is_directory_node() {
426 let node = TreeNode::dir(vec![Link {
427 hash: [1u8; 32],
428 name: Some("file.txt".to_string()),
429 size: 100,
430 key: None,
431 link_type: LinkType::Blob,
432 meta: None,
433 }]);
434 let encoded = encode_tree_node(&node).unwrap();
435
436 assert!(is_directory_node(&encoded));
437 }
438
439 #[test]
440 fn test_is_directory_node_empty() {
441 let node = TreeNode::dir(vec![]);
442 let encoded = encode_tree_node(&node).unwrap();
443
444 assert!(is_directory_node(&encoded));
445 }
446
447 #[test]
448 fn test_is_not_directory_node() {
449 let node = TreeNode::file(vec![Link::new([1u8; 32])]);
451 let encoded = encode_tree_node(&node).unwrap();
452
453 assert!(!is_directory_node(&encoded));
454 }
455
456 #[test]
457 fn test_encrypted_link_roundtrip() {
458 let hash = [1u8; 32];
459 let key = [2u8; 32];
460
461 let node = TreeNode::dir(vec![Link {
462 hash,
463 name: Some("encrypted.dat".to_string()),
464 size: 1024,
465 key: Some(key),
466 link_type: LinkType::Blob,
467 meta: None,
468 }]);
469
470 let encoded = encode_tree_node(&node).unwrap();
471 let decoded = decode_tree_node(&encoded).unwrap();
472
473 assert_eq!(decoded.links[0].key, Some(key));
474 }
475
476 #[test]
477 fn test_encoding_determinism() {
478 let hash = [42u8; 32];
481
482 let node = TreeNode::dir(vec![Link {
483 hash,
484 name: Some("file.txt".to_string()),
485 size: 100,
486 key: None,
487 link_type: LinkType::Blob,
488 meta: None,
489 }]);
490
491 let encoded1 = encode_tree_node(&node).unwrap();
493 let encoded2 = encode_tree_node(&node).unwrap();
494 let encoded3 = encode_tree_node(&node).unwrap();
495
496 assert_eq!(encoded1, encoded2, "Encoding should be deterministic");
497 assert_eq!(encoded2, encoded3, "Encoding should be deterministic");
498 }
499
500 #[test]
501 fn test_directory_link_order_is_canonicalized() {
502 let apple = Link::new([1u8; 32]).with_name("apple").with_size(1);
503 let zebra = Link::new([2u8; 32]).with_name("zebra").with_size(2);
504
505 let sorted = TreeNode::dir(vec![apple.clone(), zebra.clone()]);
506 let reversed = TreeNode::dir(vec![zebra, apple]);
507
508 let encoded_sorted = encode_tree_node(&sorted).unwrap();
509 let encoded_reversed = encode_tree_node(&reversed).unwrap();
510 assert_eq!(encoded_sorted, encoded_reversed);
511
512 let decoded = decode_tree_node(&encoded_reversed).unwrap();
513 let names = decoded
514 .links
515 .iter()
516 .map(|link| link.name.as_deref())
517 .collect::<Vec<_>>();
518 assert_eq!(names, vec![Some("apple"), Some("zebra")]);
519 }
520
521 #[test]
522 fn test_directory_link_order_uses_utf8_bytes() {
523 let bmp_private_use = Link::new([1u8; 32]).with_name("\u{E000}").with_size(1);
524 let supplementary = Link::new([2u8; 32]).with_name("\u{10000}").with_size(2);
525
526 let node = TreeNode::dir(vec![supplementary, bmp_private_use]);
527 let decoded = decode_tree_node(&encode_tree_node(&node).unwrap()).unwrap();
528 let names = decoded
529 .links
530 .iter()
531 .map(|link| link.name.as_deref())
532 .collect::<Vec<_>>();
533
534 assert_eq!(names, vec![Some("\u{E000}"), Some("\u{10000}")]);
535 }
536
537 #[test]
538 fn test_file_link_order_is_preserved() {
539 let first = Link::new([1u8; 32]).with_size(1);
540 let second = Link::new([2u8; 32]).with_size(1);
541
542 let file = TreeNode::file(vec![first.clone(), second.clone()]);
543 let reversed_file = TreeNode::file(vec![second, first]);
544
545 assert_ne!(
546 encode_tree_node(&file).unwrap(),
547 encode_tree_node(&reversed_file).unwrap()
548 );
549
550 let decoded = decode_tree_node(&encode_tree_node(&reversed_file).unwrap()).unwrap();
551 assert_eq!(decoded.links[0].hash, [2u8; 32]);
552 assert_eq!(decoded.links[1].hash, [1u8; 32]);
553 }
554
555 #[test]
556 fn test_bud17_directory_fanout_vector() {
557 let mut first_meta = HashMap::new();
558 first_meta.insert("count".to_string(), serde_json::json!(2));
559 first_meta.insert("first".to_string(), serde_json::json!("a.txt"));
560 first_meta.insert("last".to_string(), serde_json::json!("b.txt"));
561
562 let mut second_meta = HashMap::new();
563 second_meta.insert("count".to_string(), serde_json::json!(1));
564 second_meta.insert("first".to_string(), serde_json::json!("c.txt"));
565 second_meta.insert("last".to_string(), serde_json::json!("c.txt"));
566
567 let node = TreeNode::fanout(vec![
568 Link::new([0x11u8; 32])
569 .with_size(30)
570 .with_link_type(LinkType::Dir)
571 .with_meta(first_meta),
572 Link::new([0x22u8; 32])
573 .with_size(40)
574 .with_link_type(LinkType::Dir)
575 .with_meta(second_meta),
576 ]);
577
578 let encoded = encode_tree_node(&node).unwrap();
579 assert_eq!(
580 hex::encode(&encoded),
581 "82a16c9284a168c4201111111111111111111111111111111111111111111111111111111111111111a16d83a5636f756e7402a56669727374a5612e747874a46c617374a5622e747874a1731ea1740284a168c4202222222222222222222222222222222222222222222222222222222222222222a16d83a5636f756e7401a56669727374a5632e747874a46c617374a5632e747874a17328a17402a17403"
582 );
583 assert_eq!(
584 to_hex(&sha256(&encoded)),
585 "6626ab03b5468f417d888fa25fa22b48f5bcb7dfafb88eef34c638d167afc0a3"
586 );
587
588 let decoded = decode_tree_node(&encoded).unwrap();
589 assert_eq!(decoded.node_type, LinkType::Fanout);
590 assert!(decoded.links.iter().all(|link| link.name.is_none()));
591 }
592
593 #[test]
594 fn test_link_meta_determinism() {
595 let hash = [1u8; 32];
598
599 let mut meta1 = HashMap::new();
601 meta1.insert("zebra".to_string(), serde_json::json!("last"));
602 meta1.insert("alpha".to_string(), serde_json::json!("first"));
603 meta1.insert("middle".to_string(), serde_json::json!("mid"));
604
605 let mut meta2 = HashMap::new();
606 meta2.insert("alpha".to_string(), serde_json::json!("first"));
607 meta2.insert("middle".to_string(), serde_json::json!("mid"));
608 meta2.insert("zebra".to_string(), serde_json::json!("last"));
609
610 let node1 = TreeNode::dir(vec![Link::new(hash)
611 .with_name("file")
612 .with_size(100)
613 .with_meta(meta1)]);
614 let node2 = TreeNode::dir(vec![Link::new(hash)
615 .with_name("file")
616 .with_size(100)
617 .with_meta(meta2)]);
618
619 let encoded1 = encode_tree_node(&node1).unwrap();
620 let encoded2 = encode_tree_node(&node2).unwrap();
621
622 assert_eq!(
624 encoded1, encoded2,
625 "Link meta encoding should be deterministic regardless of insertion order"
626 );
627
628 let hash1 = crate::hash::sha256(&encoded1);
630 let hash2 = crate::hash::sha256(&encoded2);
631 assert_eq!(hash1, hash2, "Hashes should match for identical content");
632 }
633
634 #[test]
635 fn test_get_node_type() {
636 let dir_node = TreeNode::dir(vec![]);
637 let dir_encoded = encode_tree_node(&dir_node).unwrap();
638 assert_eq!(get_node_type(&dir_encoded), LinkType::Dir);
639
640 let file_node = TreeNode::file(vec![]);
641 let file_encoded = encode_tree_node(&file_node).unwrap();
642 assert_eq!(get_node_type(&file_encoded), LinkType::File);
643
644 let fanout_node = TreeNode::fanout(vec![]);
645 let fanout_encoded = encode_tree_node(&fanout_node).unwrap();
646 assert_eq!(get_node_type(&fanout_encoded), LinkType::Fanout);
647
648 let blob = vec![1u8, 2, 3, 4, 5];
650 assert_eq!(get_node_type(&blob), LinkType::Blob);
651 }
652
653 #[test]
654 fn test_link_type_roundtrip() {
655 let node = TreeNode::dir(vec![
656 Link::new([1u8; 32]).with_link_type(LinkType::Blob),
657 Link::new([2u8; 32]).with_link_type(LinkType::File),
658 Link::new([3u8; 32]).with_link_type(LinkType::Dir),
659 ]);
660
661 let encoded = encode_tree_node(&node).unwrap();
662 let decoded = decode_tree_node(&encoded).unwrap();
663
664 assert_eq!(decoded.links[0].link_type, LinkType::Blob);
665 assert_eq!(decoded.links[1].link_type, LinkType::File);
666 assert_eq!(decoded.links[2].link_type, LinkType::Dir);
667 }
668}