use data_encoding::BASE32_NOPAD; use fjall::UserKey; use jacquard_common::types::crypto::{PublicKey, encode_uvarint}; use jacquard_common::types::string::Did; use jacquard_common::types::tid::Tid; use jacquard_common::{CowStr, IntoStatic}; use miette::{Context, IntoDiagnostic}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use smol_str::{SmolStr, SmolStrBuilder, format_smolstr}; use std::borrow::Cow; use std::fmt::Display; const S32_CHAR: &str = "234567abcdefghijklmnopqrstuvwxyz"; #[derive(Clone, Debug, PartialEq, Eq)] pub enum TrimmedDid<'s> { Plc([u8; 15]), Web(CowStr<'s>), Other(CowStr<'s>), } pub(crate) const TAG_PLC: u8 = 1; pub(crate) const TAG_WEB: u8 = 2; impl<'s> TrimmedDid<'s> { pub fn len(&self) -> usize { match self { TrimmedDid::Plc(_) => 16, TrimmedDid::Web(s) => 1 + s.len(), TrimmedDid::Other(s) => s.len(), } } pub fn into_static(self) -> TrimmedDid<'static> { match self { TrimmedDid::Plc(bytes) => TrimmedDid::Plc(bytes), TrimmedDid::Web(s) => TrimmedDid::Web(s.into_static()), TrimmedDid::Other(s) => TrimmedDid::Other(s.into_static()), } } pub fn from_did_str(value: &'s str) -> Self { Self::from_trimmed_str(value) } fn from_trimmed_str(value: &'s str) -> Self { let value = value.strip_prefix("did:").unwrap_or(value); if let Some(rest) = value.strip_prefix("plc:") && rest.len() == 24 && let Ok(bytes) = BASE32_NOPAD.decode(rest.to_ascii_uppercase().as_bytes()) && let Ok(bytes) = bytes.try_into() { return TrimmedDid::Plc(bytes); } if let Some(rest) = value.strip_prefix("web:") { return TrimmedDid::Web(CowStr::Borrowed(rest)); } TrimmedDid::Other(CowStr::Borrowed(value)) } pub fn to_did(&self) -> Did { match self { TrimmedDid::Plc(_) => { let s = self.to_string(); Did::new_owned(format_smolstr!("did:{}", s)).expect("valid did from plc") } TrimmedDid::Web(s) => { Did::new_owned(format_smolstr!("did:web:{}", s)).expect("valid did from web") } TrimmedDid::Other(s) => { Did::new_owned(format_smolstr!("did:{}", s)).expect("valid did from other") } } } pub fn write_to_vec(&self, buf: &mut Vec) { match self { TrimmedDid::Plc(bytes) => { buf.push(TAG_PLC); buf.extend_from_slice(bytes); } TrimmedDid::Web(s) => { buf.push(TAG_WEB); buf.extend_from_slice(s.as_bytes()); } TrimmedDid::Other(s) => buf.extend_from_slice(s.as_bytes()), } } } impl Display for TrimmedDid<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TrimmedDid::Plc(bytes) => { f.write_str("plc:")?; f.write_str(&BASE32_NOPAD.encode(bytes).to_ascii_lowercase()) } TrimmedDid::Web(s) => { f.write_str("web:")?; f.write_str(s) } TrimmedDid::Other(s) => f.write_str(s), } } } impl<'a> From<&'a Did> for TrimmedDid<'a> { fn from(did: &'a Did) -> Self { Self::from_trimmed_str(did.as_str()) } } impl<'a> TryFrom<&'a [u8]> for TrimmedDid<'a> { type Error = miette::Report; fn try_from(value: &'a [u8]) -> miette::Result { if value.is_empty() { miette::bail!("empty did key"); } match value[0] { TAG_PLC => { if value.len() == 16 { let mut arr = [0u8; 15]; arr.copy_from_slice(&value[1..]); return Ok(TrimmedDid::Plc(arr)); } miette::bail!("invalid length for tagged plc did"); } TAG_WEB => { if let Ok(s) = std::str::from_utf8(&value[1..]) { return Ok(TrimmedDid::Web(CowStr::Borrowed(s))); } miette::bail!("invalid utf8 for tagged web did"); } _ => { if let Ok(s) = std::str::from_utf8(value) { return Ok(TrimmedDid::Other(CowStr::Borrowed(s))); } miette::bail!("invalid utf8 for other did"); } } } } impl<'a> From> for UserKey { fn from(val: TrimmedDid<'a>) -> Self { let mut vec = Vec::with_capacity(32); val.write_to_vec(&mut vec); UserKey::new(&vec) } } impl<'a> From<&TrimmedDid<'a>> for UserKey { fn from(val: &TrimmedDid<'a>) -> Self { let mut vec = Vec::with_capacity(32); val.write_to_vec(&mut vec); UserKey::new(&vec) } } impl Serialize for TrimmedDid<'_> { fn serialize(&self, serializer: S) -> Result { let mut vec = Vec::with_capacity(32); self.write_to_vec(&mut vec); serializer.serialize_bytes(&vec) } } impl<'de> Deserialize<'de> for TrimmedDid<'de> { fn deserialize>(deserializer: D) -> Result { struct DidVisitor; fn static_from_bytes(value: &[u8]) -> Result, E> where E: serde::de::Error, { TrimmedDid::try_from(value) .map(TrimmedDid::into_static) .map_err(E::custom) } impl<'de> serde::de::Visitor<'de> for DidVisitor { type Value = TrimmedDid<'de>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("bytes (tagged) or string (legacy)") } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { Ok(TrimmedDid::from_trimmed_str(value).into_static()) } fn visit_borrowed_str(self, value: &'de str) -> Result where E: serde::de::Error, { Ok(TrimmedDid::from_trimmed_str(value)) } fn visit_string(self, value: String) -> Result where E: serde::de::Error, { Ok(TrimmedDid::from_trimmed_str(&value).into_static()) } fn visit_bytes(self, value: &[u8]) -> Result where E: serde::de::Error, { static_from_bytes(value) } fn visit_borrowed_bytes(self, value: &'de [u8]) -> Result where E: serde::de::Error, { TrimmedDid::try_from(value).map_err(E::custom) } fn visit_byte_buf(self, value: Vec) -> Result where E: serde::de::Error, { static_from_bytes(&value) } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0).min(2048)); while let Some(byte) = seq.next_element()? { bytes.push(byte); } static_from_bytes(&bytes) } } deserializer.deserialize_any(DidVisitor) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct DbTid(#[serde(with = "serde_bytes")] [u8; 8]); impl DbTid { pub fn new_from_bytes(bytes: [u8; 8]) -> Self { Self(bytes) } pub fn len(&self) -> usize { self.0.len() } pub fn as_bytes(&self) -> &[u8] { &self.0 } pub fn to_tid(self) -> Tid { Tid::raw(self.to_smolstr()) } pub fn add_micros(self, micros: u64) -> Self { let raw = u64::from_be_bytes(self.0); let next_micros = (raw >> 10).saturating_add(micros); Self((next_micros << 10).to_be_bytes()) } fn to_smolstr(self) -> SmolStr { let mut i = u64::from_be_bytes(self.0); let mut s = SmolStrBuilder::new(); for _ in 0..13 { let c = i & 0x1F; s.push(S32_CHAR.chars().nth(c as usize).unwrap()); i >>= 5; } let mut builder = SmolStrBuilder::new(); for c in s.finish().chars().rev() { builder.push(c); } builder.finish() } } pub fn s32decode(s: &str) -> u64 { let mut i: usize = 0; for c in s.chars() { i = i * 32 + S32_CHAR.chars().position(|x| x == c).unwrap(); } i as u64 } impl From<&Tid> for DbTid { fn from(tid: &Tid) -> Self { DbTid(s32decode(tid.as_str()).to_be_bytes()) } } impl From for Tid { fn from(val: DbTid) -> Self { val.to_tid() } } impl Display for DbTid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_smolstr()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] #[cfg_attr(not(feature = "indexer"), allow(dead_code))] pub enum DbAction { Create = 0, Update = 1, Delete = 2, } #[cfg_attr(not(feature = "indexer"), allow(dead_code))] impl DbAction { pub fn as_str(&self) -> &'static str { match self { DbAction::Create => "create", DbAction::Update => "update", DbAction::Delete => "delete", } } } impl<'a> TryFrom<&'a str> for DbAction { type Error = miette::Report; fn try_from(s: &'a str) -> Result { match s { "create" => Ok(DbAction::Create), "update" => Ok(DbAction::Update), "delete" => Ok(DbAction::Delete), _ => miette::bail!("invalid action: {}", s), } } } impl Display for DbAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] #[serde(untagged)] #[cfg_attr(not(feature = "indexer"), allow(dead_code))] pub enum DbRkey { Tid(DbTid), Str(SmolStr), } impl<'de> Deserialize<'de> for DbRkey { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct DbRkeyVisitor; fn tid_from_bytes(value: &[u8]) -> Result where E: serde::de::Error, { let bytes = value .try_into() .map_err(|_| E::invalid_length(value.len(), &"exactly 8 bytes"))?; Ok(DbRkey::Tid(DbTid::new_from_bytes(bytes))) } impl<'de> serde::de::Visitor<'de> for DbRkeyVisitor { type Value = DbRkey; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("an rkey string or an 8-byte tid") } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { Ok(DbRkey::Str(SmolStr::from(value))) } fn visit_string(self, value: String) -> Result where E: serde::de::Error, { Ok(DbRkey::Str(SmolStr::from(value))) } fn visit_bytes(self, value: &[u8]) -> Result where E: serde::de::Error, { tid_from_bytes(value) } fn visit_byte_buf(self, value: Vec) -> Result where E: serde::de::Error, { tid_from_bytes(&value) } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut bytes = [0; 8]; for (index, byte) in bytes.iter_mut().enumerate() { *byte = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(index, &self))?; } if seq.next_element::()?.is_some() { return Err(serde::de::Error::invalid_length(9, &self)); } Ok(DbRkey::Tid(DbTid::new_from_bytes(bytes))) } } deserializer.deserialize_any(DbRkeyVisitor) } } #[cfg_attr(not(feature = "indexer"), allow(dead_code))] impl DbRkey { pub fn new(s: &str) -> Self { if let Ok(tid) = Tid::new(s) { DbRkey::Tid(DbTid::from(&tid)) } else { DbRkey::Str(SmolStr::from(s)) } } pub fn to_smolstr(&self) -> SmolStr { match self { DbRkey::Tid(tid) => tid.to_smolstr(), DbRkey::Str(s) => s.clone(), } } } impl From<&str> for DbRkey { fn from(s: &str) -> Self { Self::new(s) } } impl From for DbRkey { fn from(s: String) -> Self { Self::new(&s) } } impl From for DbRkey { fn from(s: SmolStr) -> Self { Self::new(s.as_str()) } } impl Display for DbRkey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_smolstr()) } } /// did:key:z... → raw multicodec public key bytes #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, jacquard_derive::IntoStatic)] pub struct DidKey<'b>( #[serde(borrow)] #[serde(with = "serde_bytes")] pub Cow<'b, [u8]>, ); impl DidKey<'_> { #[allow(dead_code)] pub fn from_did_key(s: &str) -> miette::Result { let multibase_str = s .strip_prefix("did:key:") .ok_or_else(|| miette::miette!("missing did:key: prefix in {s}"))?; let (_base, bytes) = multibase::decode(multibase_str) .into_diagnostic() .wrap_err("invalid multibase in did:key")?; Ok(Self(Cow::Owned(bytes))) } pub fn encode(&self) -> String { multibase::encode(multibase::Base::Base58Btc, &self.0) } } impl std::fmt::Display for DidKey<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "did:key:{}", self.encode()) } } impl From> for DidKey<'static> { fn from(value: PublicKey<'_>) -> Self { use jacquard_common::types::crypto::KeyCodec; fn code_of(codec: KeyCodec) -> u64 { match codec { KeyCodec::Ed25519 => 0xED, KeyCodec::Secp256k1 => 0xE7, KeyCodec::P256 => 0x1200, KeyCodec::Unknown(c) => c, } } let mut bytes = Vec::with_capacity(8 + value.bytes.len()); bytes.append(&mut encode_uvarint(code_of(value.codec))); bytes.extend_from_slice(&value.bytes); Self(bytes.into()) } } #[cfg(test)] mod tests { use super::*; use jacquard_common::types::tid::Tid; #[test] fn test_dbtid_roundtrip() { let tid_str = "3jzfcijpj2z2a"; let tid = Tid::new(tid_str).unwrap(); let db_tid = DbTid::from(&tid); assert_eq!(db_tid.to_tid().as_str(), tid_str); let tid_str_2 = "2222222222222"; let tid = Tid::new(tid_str_2).unwrap(); let db_tid = DbTid::from(&tid); assert_eq!(db_tid.to_tid().as_str(), tid_str_2); } #[test] fn db_representation_conversions_are_lossless() { for did in [ "did:plc:ewvi7nxzyoun6zhxrhs64oiz", "did:web:example.com", "did:example:opaque", ] { let did = Did::new_owned(did).unwrap(); let trimmed = TrimmedDid::from(&did); let mut bytes = Vec::new(); trimmed.write_to_vec(&mut bytes); assert_eq!(TrimmedDid::try_from(bytes.as_slice()).unwrap(), trimmed); assert_eq!(trimmed.to_did(), did); assert_eq!(trimmed.to_string(), did.as_str().trim_start_matches("did:")); assert_eq!( UserKey::from(trimmed.clone()), UserKey::from(&trimmed), "owned and borrowed key conversions diverged for {did}" ); assert_eq!(trimmed.clone().into_static(), trimmed); } for malformed in [ &[][..], &[TAG_PLC][..], &[TAG_PLC, 0][..], &[TAG_WEB, 0xff][..], &[0xff][..], ] { assert!(TrimmedDid::try_from(malformed).is_err()); } let tid = Tid::new("3jzfcijpj2z2a").unwrap(); let db_tid = DbTid::from(&tid); assert_eq!(Tid::from(db_tid), tid); assert_eq!(db_tid.to_string(), tid.as_str()); for value in ["self", "stinkpot", "3jzfcijpj2z2a"] { let from_str = DbRkey::from(value); assert_eq!(DbRkey::from(value.to_owned()), from_str); assert_eq!(DbRkey::from(SmolStr::from(value)), from_str); assert_eq!(from_str.to_string(), value); } use jacquard_common::types::crypto::KeyCodec; for (codec, prefix) in [ (KeyCodec::Ed25519, &[0xed, 0x01][..]), (KeyCodec::Secp256k1, &[0xe7, 0x01][..]), (KeyCodec::P256, &[0x80, 0x24][..]), (KeyCodec::Unknown(0x42), &[0x42][..]), ] { let key = DidKey::from(PublicKey { codec, bytes: Cow::Borrowed(&[1, 2, 3]), }); assert_eq!(key.0.strip_suffix(&[1, 2, 3]), Some(prefix)); assert_eq!(DidKey::from_did_key(&key.to_string()).unwrap(), key); } } #[test] fn test_dbrkey() { let tid_str = "3jzfcijpj2z2a"; let rkey = DbRkey::new(tid_str); if let DbRkey::Tid(t) = rkey { assert_eq!(t.to_tid().as_str(), tid_str); } else { panic!("expected tid"); } let str_val = "self"; let rkey = DbRkey::new(str_val); if let DbRkey::Str(s) = rkey { assert_eq!(s, str_val); } else { panic!("expected str"); } } #[test] fn dbrkey_msgpack_string_marker_preserves_eight_byte_rkey() { let encoded = rmp_serde::to_vec(&DbRkey::Str("stinkpot".into())).unwrap(); assert_eq!(encoded, b"\xa8stinkpot"); let decoded: DbRkey = rmp_serde::from_slice(&encoded).unwrap(); assert_eq!(decoded, DbRkey::Str("stinkpot".into())); assert_eq!(decoded.to_smolstr(), "stinkpot"); } #[test] fn dbrkey_msgpack_binary_marker_preserves_tid() { let expected = DbRkey::new("3jzfcijpj2z2a"); let encoded = rmp_serde::to_vec(&expected).unwrap(); assert_eq!(encoded.get(..2), Some([0xc4, 8].as_slice())); let decoded: DbRkey = rmp_serde::from_slice(&encoded).unwrap(); assert_eq!(decoded, expected); assert_eq!(decoded.to_smolstr(), "3jzfcijpj2z2a"); } #[test] fn dbrkey_json_tid_sequence_stays_compatible() { let expected = DbRkey::new("3jzfcijpj2z2a"); let encoded = serde_json::to_vec(&expected).unwrap(); assert_eq!(encoded.first(), Some(&b'[')); let decoded: DbRkey = serde_json::from_slice(&encoded).unwrap(); assert_eq!(decoded, expected); } #[test] fn trimmed_did_accepts_legacy_msgpack_strings() { for did in [ "did:plc:ewvi7nxzyoun6zhxrhs64oiz", "did:web:example.com", "did:example:opaque", ] { let parsed = Did::new_owned(did).unwrap(); let expected = TrimmedDid::from(&parsed); for legacy in [did, did.strip_prefix("did:").unwrap()] { let encoded = rmp_serde::to_vec(legacy).unwrap(); let decoded: TrimmedDid<'_> = rmp_serde::from_slice(&encoded).unwrap(); assert_eq!(decoded, expected); assert_eq!(decoded.to_did().as_str(), did); } } } #[test] fn trimmed_did_binary_and_json_sequence_roundtrip() { for (did, golden) in [ ( "did:plc:ewvi7nxzyoun6zhxrhs64oiz", "c4100125aa8fb6f9c3a8df64f789e5ee3919", ), ("did:web:example.com", "c40c026578616d706c652e636f6d"), ("did:example:opaque", "c40e6578616d706c653a6f7061717565"), ] { let parsed = Did::new_owned(did).unwrap(); let expected = TrimmedDid::from(&parsed); let msgpack = rmp_serde::to_vec(&expected).unwrap(); assert_eq!(msgpack.first(), Some(&0xc4)); assert_eq!(hex::encode(&msgpack), golden); let from_msgpack: TrimmedDid<'_> = rmp_serde::from_slice(&msgpack).unwrap(); assert_eq!(from_msgpack, expected); let json = serde_json::to_vec(&expected).unwrap(); assert_eq!(json.first(), Some(&b'[')); let from_json: TrimmedDid<'_> = serde_json::from_slice(&json).unwrap(); assert_eq!(from_json, expected); } } #[test] fn dbrkey_string_markers_preserve_every_utf8_length() { let mut values = (0..=24) .map(|length| "x".repeat(length)) .collect::>(); values.extend(["stinkpot".to_owned(), "éééé".to_owned()]); for value in values { let expected = DbRkey::Str(value.clone().into()); let encoded = rmp_serde::to_vec(&expected).unwrap(); let decoded: DbRkey = rmp_serde::from_slice(&encoded).unwrap(); assert_eq!(decoded, expected, "failed for {value:?}"); } } #[test] fn dbrkey_binary_marker_requires_exactly_eight_bytes() { for length in [0, 1, 7, 9, 16] { let encoded = rmp_serde::to_vec(&serde_bytes::ByteBuf::from(vec![b'x'; length])).unwrap(); assert!(rmp_serde::from_slice::(&encoded).is_err()); } let encoded = rmp_serde::to_vec(&serde_bytes::ByteBuf::from(b"stinkpot".to_vec())).unwrap(); let decoded: DbRkey = rmp_serde::from_slice(&encoded).unwrap(); assert!(matches!(decoded, DbRkey::Tid(_))); } #[test] fn dbrkey_json_sequence_requires_exactly_eight_bytes() { for length in [0, 1, 7, 9, 16] { let encoded = serde_json::to_vec(&vec![0_u8; length]).unwrap(); assert!(serde_json::from_slice::(&encoded).is_err()); } let decoded: DbRkey = serde_json::from_slice(b"[0,1,2,3,4,5,6,7]").unwrap(); assert_eq!( decoded, DbRkey::Tid(DbTid::new_from_bytes([0, 1, 2, 3, 4, 5, 6, 7])) ); } #[test] fn db_action_and_did_key_representation_contracts() { for (action, golden) in [ (DbAction::Create, "a6437265617465"), (DbAction::Update, "a6557064617465"), (DbAction::Delete, "a644656c657465"), ] { let msgpack = rmp_serde::to_vec(&action).unwrap(); assert_eq!(hex::encode(&msgpack), golden); assert_eq!(rmp_serde::from_slice::(&msgpack).unwrap(), action); assert_eq!(DbAction::try_from(action.as_str()).unwrap(), action); } let expected = DidKey(Cow::Borrowed(&[0xed, 0x01, 0x02, 0x03])); let msgpack = rmp_serde::to_vec(&expected).unwrap(); assert_eq!(hex::encode(&msgpack), "c404ed010203"); let decoded: DidKey<'_> = rmp_serde::from_slice(&msgpack).unwrap(); assert_eq!(decoded, expected); let display = expected.to_string(); assert_eq!(DidKey::from_did_key(&display).unwrap(), expected); } }