shard-core 2.1.0

Core library for shard distributed VCS: chunking, compression, commits, branching, merging, WAL
Documentation
use serde::{Deserialize, Serialize};

fn default_compression() -> String {
    "none".to_string()
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FileManifest {
    pub name: String,
    pub size: u64,
    pub chunks: Vec<String>,
    pub content_type: Option<String>,
    #[serde(default = "default_compression")]
    pub compression: String,
    #[serde(default)]
    pub merkle_root: Option<String>,
    #[serde(default)]
    pub created_by: Option<String>,
    #[serde(default)]
    pub created_at: Option<u64>,
    #[serde(default)]
    pub signature: Option<String>,
}

impl FileManifest {
    pub fn merkle_root(chunk_hashes: &[String]) -> String {
        let mut hasher = blake3::Hasher::new();
        for h in chunk_hashes {
            hasher.update(h.as_bytes());
        }
        hasher.finalize().to_hex().to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_manifest_serialization_roundtrip() {
        let m = FileManifest {
            name: "test.bin".into(),
            size: 1024,
            chunks: vec!["chunk1".into(), "chunk2".into()],
            content_type: Some("application/octet-stream".into()),
            compression: "zstd".into(),
            merkle_root: Some("merklehash".into()),
            created_by: Some("user".into()),
            created_at: Some(1000),
            signature: Some("sig".into()),
        };
        let json = serde_json::to_vec(&m).unwrap();
        let m2: FileManifest = serde_json::from_slice(&json).unwrap();
        assert_eq!(m2.name, "test.bin");
        assert_eq!(m2.size, 1024);
        assert_eq!(m2.chunks.len(), 2);
        assert_eq!(m2.compression, "zstd");
    }

    #[test]
    fn test_manifest_backward_compat() {
        let json = r#"{"name":"f.txt","size":50,"chunks":["h1"]}"#;
        let m: FileManifest = serde_json::from_str(json).unwrap();
        assert_eq!(m.compression, "none");
        assert!(m.content_type.is_none());
        assert!(m.merkle_root.is_none());
        assert!(m.created_by.is_none());
        assert!(m.signature.is_none());
    }

    #[test]
    fn test_manifest_default_compression() {
        let m = FileManifest {
            name: "n".into(),
            size: 0,
            chunks: vec![],
            content_type: None,
            compression: "none".into(),
            merkle_root: None,
            created_by: None,
            created_at: None,
            signature: None,
        };
        assert_eq!(m.compression, "none");
    }

    #[test]
    fn test_manifest_merkle_root_deterministic() {
        let chunks = vec!["a".into(), "b".into()];
        let root1 = FileManifest::merkle_root(&chunks);
        let root2 = FileManifest::merkle_root(&chunks);
        assert_eq!(root1, root2);
    }

    #[test]
    fn test_manifest_merkle_root_order_matters() {
        let ab = FileManifest::merkle_root(&["a".into(), "b".into()]);
        let ba = FileManifest::merkle_root(&["b".into(), "a".into()]);
        assert_ne!(ab, ba);
    }

    #[test]
    fn test_manifest_merkle_root_empty() {
        let root = FileManifest::merkle_root(&[]);
        assert!(!root.is_empty());
    }

    #[test]
    fn test_manifest_all_fields_json() {
        let m = FileManifest {
            name: "all.bin".into(),
            size: 999,
            chunks: vec!["c1".into(), "c2".into(), "c3".into()],
            content_type: Some("application/x-model".into()),
            compression: "zlib".into(),
            merkle_root: Some("mr".into()),
            created_by: Some("author".into()),
            created_at: Some(12345),
            signature: Some("signed".into()),
        };
        let json = serde_json::to_string_pretty(&m).unwrap();
        let m2: FileManifest = serde_json::from_str(&json).unwrap();
        assert_eq!(m2.content_type.unwrap(), "application/x-model");
        assert_eq!(m2.created_by.unwrap(), "author");
        assert_eq!(m2.created_at.unwrap(), 12345);
        assert_eq!(m2.signature.unwrap(), "signed");
    }
}