libvctrl_core 1.1.0

Reference implementations of the libvctrl contracts (in-memory store, SHA-512 hasher, binary codec)
Documentation
use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder};
use libvctrl_handler::{
    Blob, Commit, Decoder, Encoder, EntryKind, Hash, Tag, Tree, TreeEntry, UserID,
};
use proptest::prelude::*;

fn hash_strategy() -> impl Strategy<Value = Hash> {
    any::<[u8; 64]>().prop_map(|bytes| Hash::from_bytes(&bytes).unwrap())
}

fn name_strategy() -> impl Strategy<Value = String> {
    proptest::string::string_regex("[a-zA-Z][a-zA-Z0-9]{0,30}")
        .unwrap()
        .prop_filter("name must not be '.' or '..'", |s| s != "." && s != "..")
}

fn email_strategy() -> impl Strategy<Value = String> {
    proptest::string::string_regex("[a-z]{1,10}@[a-z]{1,10}\\.[a-z]{2,4}").unwrap()
}

fn user_id_strategy() -> impl Strategy<Value = UserID> {
    (name_strategy(), email_strategy()).prop_map(|(name, email)| UserID::new(name, email).unwrap())
}

fn blob_strategy() -> impl Strategy<Value = Blob> {
    proptest::collection::vec(any::<u8>(), 0..64 * 1024).prop_map(Blob::new)
}

fn tree_entry_strategy() -> impl Strategy<Value = TreeEntry> {
    (name_strategy(), any::<u8>(), hash_strategy()).prop_map(|(name, kind_byte, hash)| {
        let kind = if kind_byte % 2 == 0 {
            EntryKind::Blob
        } else {
            EntryKind::Tree
        };
        TreeEntry::new(name, kind, hash).unwrap()
    })
}

fn tree_strategy() -> impl Strategy<Value = Tree> {
    proptest::collection::vec(tree_entry_strategy(), 0..20).prop_map(|mut entries| {
        entries.sort_by(|a, b| a.name().cmp(b.name()));
        entries.dedup_by(|a, b| a.name() == b.name());
        Tree::new(entries).unwrap()
    })
}

fn message_strategy() -> impl Strategy<Value = String> {
    proptest::string::string_regex("[a-zA-Z0-9 ]{0,1000}").unwrap()
}

fn commit_strategy() -> impl Strategy<Value = Commit> {
    (
        hash_strategy(),
        proptest::collection::vec(hash_strategy(), 0..3),
        user_id_strategy(),
        user_id_strategy(),
        message_strategy(),
    )
        .prop_map(|(tree, parents, author, committer, message)| {
            Commit::new(tree, parents, author, committer, message)
        })
}

fn tag_strategy() -> impl Strategy<Value = Tag> {
    (
        name_strategy(),
        hash_strategy(),
        proptest::option::of(user_id_strategy()),
        message_strategy(),
    )
        .prop_map(|(name, target, tagger, message)| {
            Tag::new(name, target, tagger, message).unwrap()
        })
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(1000))]

    #[test]
    fn prop_roundtrip_blob(blob in blob_strategy()) {
        let encoded = BinaryEncoder.encode_blob(&blob).unwrap();
        let decoded = BinaryDecoder.decode_blob(&encoded).unwrap();
        assert_eq!(decoded.data(), blob.data());
    }

    #[test]
    fn prop_roundtrip_tree(tree in tree_strategy()) {
        let encoded = BinaryEncoder.encode_tree(&tree).unwrap();
        let decoded = BinaryDecoder.decode_tree(&encoded).unwrap();
        assert_eq!(decoded.entries().len(), tree.entries().len());
        for (a, b) in tree.entries().iter().zip(decoded.entries().iter()) {
            assert_eq!(a.name(), b.name());
            assert_eq!(a.kind(), b.kind());
            assert_eq!(a.hash(), b.hash());
        }
    }

    #[test]
    fn prop_roundtrip_commit(commit in commit_strategy()) {
        let encoded = BinaryEncoder.encode_commit(&commit).unwrap();
        let decoded = BinaryDecoder.decode_commit(&encoded).unwrap();
        assert_eq!(commit.tree(), decoded.tree());
        assert_eq!(commit.parents().len(), decoded.parents().len());
        assert_eq!(commit.author().name(), decoded.author().name());
        assert_eq!(commit.committer().name(), decoded.committer().name());
        assert_eq!(commit.message(), decoded.message());
    }

    #[test]
    fn prop_roundtrip_tag(tag in tag_strategy()) {
        let encoded = BinaryEncoder.encode_tag(&tag).unwrap();
        let decoded = BinaryDecoder.decode_tag(&encoded).unwrap();
        assert_eq!(tag.name(), decoded.name());
        assert_eq!(tag.target(), decoded.target());
        assert_eq!(tag.tagger().map(|u| u.name().to_string()), decoded.tagger().map(|u| u.name().to_string()));
        assert_eq!(tag.message(), decoded.message());
    }
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(500))]

    #[test]
    fn prop_fuzz_blob(blob in blob_strategy(), truncate_pos in any::<usize>()) {
        let mut encoded = BinaryEncoder.encode_blob(&blob).unwrap();
        if encoded.is_empty() {
            return Ok(());
        }
        let idx = truncate_pos % encoded.len();
        encoded.truncate(idx);
        assert!(BinaryDecoder.decode_blob(&encoded).is_err());
    }

    #[test]
    fn prop_fuzz_tree(tree in tree_strategy(), truncate_pos in any::<usize>()) {
        let mut encoded = BinaryEncoder.encode_tree(&tree).unwrap();
        if encoded.is_empty() {
            return Ok(());
        }
        let idx = truncate_pos % encoded.len();
        encoded.truncate(idx);
        assert!(BinaryDecoder.decode_tree(&encoded).is_err());
    }

    #[test]
    fn prop_fuzz_commit(commit in commit_strategy(), truncate_pos in any::<usize>()) {
        let mut encoded = BinaryEncoder.encode_commit(&commit).unwrap();
        if encoded.is_empty() {
            return Ok(());
        }
        let idx = truncate_pos % encoded.len();
        encoded.truncate(idx);
        assert!(BinaryDecoder.decode_commit(&encoded).is_err());
    }

    #[test]
    fn prop_fuzz_tag(tag in tag_strategy(), truncate_pos in any::<usize>()) {
        let mut encoded = BinaryEncoder.encode_tag(&tag).unwrap();
        if encoded.is_empty() {
            return Ok(());
        }
        let idx = truncate_pos % encoded.len();
        encoded.truncate(idx);
        assert!(BinaryDecoder.decode_tag(&encoded).is_err());
    }
}