hashtree-core 0.2.42

Simple content-addressed merkle tree with KV storage
Documentation
use std::collections::HashMap;

use hashtree_core::{decode_tree_node, encode_tree_node, Link, LinkType, TreeNode};
use proptest::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};

fn arb_hash() -> impl Strategy<Value = [u8; 32]> {
    any::<[u8; 32]>()
}

fn arb_json_value() -> impl Strategy<Value = Value> {
    prop_oneof![
        Just(Value::Null),
        any::<bool>().prop_map(Value::Bool),
        any::<i64>().prop_map(|n| Value::Number(Number::from(n))),
        "[a-z0-9_\\-]{0,16}".prop_map(Value::String),
    ]
}

fn arb_meta() -> impl Strategy<Value = Option<HashMap<String, Value>>> {
    prop::option::of(
        prop::collection::vec(
            ("[a-z]{1,6}".prop_map(|s| s.to_string()), arb_json_value()),
            0..6,
        )
        .prop_map(|entries| entries.into_iter().collect::<HashMap<_, _>>()),
    )
}

fn arb_link_type() -> impl Strategy<Value = LinkType> {
    prop_oneof![
        Just(LinkType::Blob),
        Just(LinkType::File),
        Just(LinkType::Dir),
    ]
}

fn arb_link() -> impl Strategy<Value = Link> {
    (
        arb_hash(),
        prop::option::of("[a-z0-9_\\-.]{1,16}"),
        any::<u16>().prop_map(u64::from),
        prop::option::of(any::<[u8; 32]>()),
        arb_link_type(),
        arb_meta(),
    )
        .prop_map(|(hash, name, size, key, link_type, meta)| Link {
            hash,
            name,
            size,
            key,
            link_type,
            meta,
        })
}

fn arb_node() -> impl Strategy<Value = TreeNode> {
    (
        prop_oneof![Just(LinkType::File), Just(LinkType::Dir)],
        prop::collection::vec(arb_link(), 0..8),
    )
        .prop_map(|(node_type, links)| TreeNode { node_type, links })
}

proptest! {
    #[test]
    fn prop_decode_encode_roundtrip(node in arb_node()) {
        let encoded = encode_tree_node(&node).unwrap();
        let decoded = decode_tree_node(&encoded).unwrap();
        prop_assert_eq!(decoded, node);
    }

    #[test]
    fn prop_encode_deterministic(node in arb_node()) {
        let encoded1 = encode_tree_node(&node).unwrap();
        let encoded2 = encode_tree_node(&node).unwrap();
        let encoded3 = encode_tree_node(&node).unwrap();
        prop_assert_eq!(encoded1, encoded2.clone());
        prop_assert_eq!(encoded2, encoded3);
    }
}

#[test]
fn test_metadata_insertion_order_does_not_change_encoding() {
    let mut meta1 = HashMap::new();
    meta1.insert("a".to_string(), Value::String("1".to_string()));
    meta1.insert("b".to_string(), Value::String("2".to_string()));
    meta1.insert("c".to_string(), Value::String("3".to_string()));

    let mut meta2 = HashMap::new();
    meta2.insert("c".to_string(), Value::String("3".to_string()));
    meta2.insert("a".to_string(), Value::String("1".to_string()));
    meta2.insert("b".to_string(), Value::String("2".to_string()));

    let link1 = Link {
        hash: [7u8; 32],
        name: Some("x".to_string()),
        size: 1,
        key: None,
        link_type: LinkType::Blob,
        meta: Some(meta1),
    };
    let link2 = Link {
        hash: [7u8; 32],
        name: Some("x".to_string()),
        size: 1,
        key: None,
        link_type: LinkType::Blob,
        meta: Some(meta2),
    };

    let node1 = TreeNode::dir(vec![link1]);
    let node2 = TreeNode::dir(vec![link2]);

    let enc1 = encode_tree_node(&node1).unwrap();
    let enc2 = encode_tree_node(&node2).unwrap();
    assert_eq!(enc1, enc2);
}

#[derive(Serialize, Deserialize)]
struct RawWireLink {
    #[serde(with = "serde_bytes")]
    h: Vec<u8>,
    s: u64,
    t: u8,
}

#[derive(Serialize, Deserialize)]
struct RawWireTreeNode {
    l: Vec<RawWireLink>,
    t: u8,
}

#[test]
fn test_invalid_node_type_rejected() {
    let raw = RawWireTreeNode {
        l: vec![RawWireLink {
            h: vec![0u8; 32],
            s: 1,
            t: 0,
        }],
        t: 9,
    };

    let bytes = rmp_serde::to_vec_named(&raw).unwrap();
    let err = decode_tree_node(&bytes).unwrap_err();
    let msg = err.to_string();
    assert!(
        msg.contains("Invalid node type"),
        "unexpected error message: {msg}"
    );
}

#[test]
fn test_unknown_link_type_defaults_to_blob() {
    let raw = RawWireTreeNode {
        l: vec![RawWireLink {
            h: vec![1u8; 32],
            s: 42,
            t: 99,
        }],
        t: 2,
    };

    let bytes = rmp_serde::to_vec_named(&raw).unwrap();
    let decoded = decode_tree_node(&bytes).unwrap();
    assert_eq!(decoded.node_type, LinkType::Dir);
    assert_eq!(decoded.links.len(), 1);
    assert_eq!(decoded.links[0].link_type, LinkType::Blob);
}