Skip to main content

hashtree_core/
codec.rs

1//! MessagePack encoding/decoding for tree nodes
2//!
3//! Blobs are stored raw (not wrapped) for efficiency.
4//! Tree nodes are MessagePack-encoded.
5//!
6//! **Determinism:** Unlike CBOR, MessagePack doesn't have a built-in canonical encoding.
7//! We ensure deterministic output by:
8//! 1. Using fixed struct field order (Rust declaration order via serde)
9//! 2. Converting HashMap metadata to BTreeMap before encoding (sorted keys)
10//! 3. Sorting directory links by BUD-16 name order before encoding
11//!
12//! File-node link order is preserved because chunk order is semantic.
13//!
14//! Format uses short keys for compact encoding:
15//! - t: type (1 = File, 2 = Dir, 3 = Fanout) - node type
16//! - l: links array
17//! - h: hash (in link)
18//! - t: type (in link, 0 = Blob, 1 = File, 2 = Dir, 3 = Fanout)
19//! - n: name (in link, optional)
20//! - s: size (in link)
21//! - m: metadata (optional)
22
23use serde::{Deserialize, Serialize};
24use std::collections::{BTreeMap, HashMap};
25
26use crate::hash::sha256;
27use crate::types::{Hash, Link, LinkType, TreeNode};
28
29/// Error type for codec operations
30#[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/// Wire format for a link (compact keys)
49/// Fields are ordered alphabetically for canonical encoding: h, k?, m?, n?, s, t
50#[derive(Serialize, Deserialize)]
51struct WireLink {
52    /// Hash (required) - use serde_bytes for proper MessagePack binary encoding
53    #[serde(with = "serde_bytes")]
54    h: Vec<u8>,
55    /// Encryption key (optional) - use serde_bytes for proper MessagePack binary encoding
56    #[serde(
57        default,
58        skip_serializing_if = "Option::is_none",
59        with = "option_bytes"
60    )]
61    k: Option<Vec<u8>>,
62    /// Metadata (optional) - uses BTreeMap for deterministic key ordering
63    #[serde(skip_serializing_if = "Option::is_none")]
64    m: Option<BTreeMap<String, serde_json::Value>>,
65    /// Name (optional)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    n: Option<String>,
68    /// Size (required)
69    s: u64,
70    /// Link type (0 = Blob, 1 = File, 2 = Dir, 3 = Fanout)
71    #[serde(default)]
72    t: u8,
73}
74
75/// Helper module for optional bytes serialization
76mod 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/// Wire format for a tree node (compact keys)
99/// Fields are ordered alphabetically for canonical encoding: l, t
100#[derive(Serialize, Deserialize)]
101struct WireTreeNode {
102    /// Links
103    l: Vec<WireLink>,
104    /// Type (1 = File, 2 = Dir, 3 = Fanout)
105    t: u8,
106}
107
108/// Loose tree shape used to distinguish raw blobs from unsupported tree types.
109#[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
125/// Encode a tree node to MessagePack
126pub 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                // Convert HashMap to BTreeMap for deterministic key ordering
134                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
166/// Decode MessagePack to a tree node
167pub 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    // Validate node type (must be File=1, Dir=2, or Fanout=3)
172    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        // Convert BTreeMap back to HashMap for the public API
196        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
211/// Encode a tree node and compute its hash
212pub 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
218/// Try to decode data as a tree node
219/// Returns Some(TreeNode) if valid tree node, None otherwise
220/// This is preferred over is_tree_node() to avoid double decoding
221pub fn try_decode_tree_node(data: &[u8]) -> Option<TreeNode> {
222    decode_tree_node(data).ok()
223}
224
225/// Get the type of data (Blob, File, Dir, or Fanout)
226/// Returns LinkType::Blob for raw blobs that aren't tree nodes
227pub 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
233/// Check if data is a MessagePack-encoded tree node (vs raw blob)
234/// Tree-shaped data with unsupported types still counts as a tree node so
235/// callers that subsequently decode it surface a codec error instead of
236/// reinterpreting the bytes as a raw blob.
237/// Note: Prefer try_decode_tree_node() to avoid double decoding
238pub 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
246/// Check if data is a directory-like tree node (node_type == Dir or Fanout)
247/// Note: Prefer try_decode_tree_node() to avoid double decoding
248pub 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        // A File node is not a directory
450        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        // Test that encoding is deterministic across multiple calls
479        // This is critical for content-addressed storage where hash must be stable
480        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        // Encode multiple times and verify identical output
492        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        // Test that link meta encoding is deterministic regardless of HashMap insertion order
596        // We use BTreeMap internally to ensure sorted keys
597        let hash = [1u8; 32];
598
599        // Create meta with keys in different orders
600        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        // Both should produce identical bytes (keys sorted alphabetically)
623        assert_eq!(
624            encoded1, encoded2,
625            "Link meta encoding should be deterministic regardless of insertion order"
626        );
627
628        // Verify the hash is also identical
629        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        // Raw blob returns Blob type
649        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}