diff --git a/packages/protocol/Cargo.toml b/packages/protocol/Cargo.toml index 5dd479a..6c3316e 100644 --- a/packages/protocol/Cargo.toml +++ b/packages/protocol/Cargo.toml @@ -14,3 +14,4 @@ jacquard-lexicon = { version = "=0.12.2", default-features = false } poem-openapi = "=5.1.16" serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" diff --git a/packages/protocol/src/records.rs b/packages/protocol/src/records.rs index 56a7f63..d6853ba 100644 --- a/packages/protocol/src/records.rs +++ b/packages/protocol/src/records.rs @@ -1,6 +1,7 @@ pub use atrium_api::types::TypedBlobRef as BlobRef; use jacquard_derive::LexiconSchema; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; pub const FILE_COLLECTION: &str = "network.crate.file"; pub const VERSION_COLLECTION: &str = "network.crate.fileVersion"; @@ -8,6 +9,12 @@ pub const NAME_COLLECTION: &str = "network.crate.fileName"; pub const SPACE_TYPE: &str = "network.crate.drive"; pub const MAX_FILE_BYTES: usize = 1_073_741_824; +/// Lowercase SHA-256 of the compact UTF-8 JSON array `[parentId-or-null, name]`. +pub fn name_claim_key(parent_id: Option<&str>, name: &str) -> String { + let input = serde_json::to_vec(&(parent_id, name)).expect("strings always serialize"); + format!("{:x}", Sha256::digest(input)) +} + /// A mutable file head, addressed by its stable record key in a private drive space. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, LexiconSchema)] #[serde(rename_all = "camelCase")] diff --git a/packages/protocol/tests/contracts.rs b/packages/protocol/tests/contracts.rs index 5dcf592..60f8469 100644 --- a/packages/protocol/tests/contracts.rs +++ b/packages/protocol/tests/contracts.rs @@ -141,3 +141,25 @@ fn metadata_update_requires_name_and_revision() { let update = UpdateFile::parse_from_json(Some(json!({"revision":CID,"name":"New"}))).unwrap(); assert!(update.parent_id.is_none()); } + +#[test] +fn name_claims_are_scoped_and_unambiguous() { + use crate_protocol::name_claim_key; + assert_eq!( + name_claim_key(None, "Documents"), + "cc78b9e5ea9ff8bd300f79190f7f7754b7befd1b77d87460dcf77288c4f7e27f" + ); + assert_eq!( + name_claim_key(Some("3mabcdef12345"), "café.txt"), + "7d91c5e0f4fdb569e9fad9dc48bb18b7ca66fa09824eed7eddeb17e4a53d2961" + ); + assert_ne!( + name_claim_key(None, "Documents"), + name_claim_key(Some("parent"), "Documents") + ); + assert_ne!( + name_claim_key(Some("ab"), "c"), + name_claim_key(Some("a"), "bc") + ); + assert_ne!(name_claim_key(None, "A"), name_claim_key(None, "a")); +}