Renames crate directories and sweeps identifiers so Plan 1B can reference
the post-rename names throughout.
- git mv crates/idfoto-{core,cli,wasm} → crates/relicario-{core,cli,wasm}
- sed sweep: idfoto_core/idfoto-core/IdfotoError/IDFOTO_IMAGE/.idfoto/ etc.
- All 128 relicario-core tests pass post-sweep
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
3.4 KiB
Rust
125 lines
3.4 KiB
Rust
//! Random and content-addressed identifiers for items, fields, and attachments.
|
|
//!
|
|
//! - `ItemId` and `FieldId` are random 16-char hex strings (64 bits of entropy)
|
|
//! generated via `OsRng` (audit M8: bumped from the v1 8-char/32-bit format).
|
|
//! - `AttachmentId` is the first 16 hex chars of `sha256(plaintext)` —
|
|
//! content-addressed so identical plaintext blobs deduplicate naturally in git.
|
|
|
|
use rand::rngs::OsRng;
|
|
use rand::RngCore;
|
|
use sha2::{Digest, Sha256};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct ItemId(pub String);
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct FieldId(pub String);
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct AttachmentId(pub String);
|
|
|
|
impl ItemId {
|
|
pub fn new() -> Self {
|
|
let mut bytes = [0u8; 8];
|
|
OsRng.fill_bytes(&mut bytes);
|
|
Self(hex::encode(bytes))
|
|
}
|
|
pub fn as_str(&self) -> &str { &self.0 }
|
|
}
|
|
|
|
impl Default for ItemId {
|
|
fn default() -> Self { Self::new() }
|
|
}
|
|
|
|
impl FieldId {
|
|
pub fn new() -> Self {
|
|
let mut bytes = [0u8; 8];
|
|
OsRng.fill_bytes(&mut bytes);
|
|
Self(hex::encode(bytes))
|
|
}
|
|
pub fn as_str(&self) -> &str { &self.0 }
|
|
}
|
|
|
|
impl Default for FieldId {
|
|
fn default() -> Self { Self::new() }
|
|
}
|
|
|
|
impl AttachmentId {
|
|
pub fn from_plaintext(plaintext: &[u8]) -> Self {
|
|
let digest = Sha256::digest(plaintext);
|
|
Self(hex::encode(&digest[..8]))
|
|
}
|
|
pub fn as_str(&self) -> &str { &self.0 }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn item_id_is_16_hex_chars() {
|
|
let id = ItemId::new();
|
|
assert_eq!(id.0.len(), 16);
|
|
assert!(id.0.chars().all(|c| c.is_ascii_hexdigit()));
|
|
}
|
|
|
|
#[test]
|
|
fn item_ids_are_unique() {
|
|
let mut seen = std::collections::HashSet::new();
|
|
for _ in 0..10_000 {
|
|
assert!(seen.insert(ItemId::new().0));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn field_id_is_16_hex_chars() {
|
|
let id = FieldId::new();
|
|
assert_eq!(id.0.len(), 16);
|
|
assert!(id.0.chars().all(|c| c.is_ascii_hexdigit()));
|
|
}
|
|
|
|
#[test]
|
|
fn field_ids_are_unique() {
|
|
let mut seen = std::collections::HashSet::new();
|
|
for _ in 0..10_000 {
|
|
assert!(seen.insert(FieldId::new().0));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn attachment_id_is_deterministic() {
|
|
let plaintext = b"hello world";
|
|
let a = AttachmentId::from_plaintext(plaintext);
|
|
let b = AttachmentId::from_plaintext(plaintext);
|
|
assert_eq!(a, b);
|
|
}
|
|
|
|
#[test]
|
|
fn attachment_id_changes_with_plaintext() {
|
|
let a = AttachmentId::from_plaintext(b"hello");
|
|
let b = AttachmentId::from_plaintext(b"world");
|
|
assert_ne!(a, b);
|
|
}
|
|
|
|
#[test]
|
|
fn attachment_id_is_16_hex_chars() {
|
|
let id = AttachmentId::from_plaintext(b"any bytes");
|
|
assert_eq!(id.0.len(), 16);
|
|
assert!(id.0.chars().all(|c| c.is_ascii_hexdigit()));
|
|
}
|
|
|
|
#[test]
|
|
fn ids_serialize_as_bare_strings() {
|
|
let item = ItemId("abcdef0123456789".to_string());
|
|
let json = serde_json::to_string(&item).unwrap();
|
|
assert_eq!(json, "\"abcdef0123456789\"");
|
|
|
|
let parsed: ItemId = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed, item);
|
|
}
|
|
}
|