Something went wrong. Try again.
Rust and WASM did-method-plc tools and structures
Something went wrong. Try again.
7.7 kB · 263 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264//! DID (Decentralized Identifier) types and validation for did:plc
use crate::encoding::is_valid_base32;use crate::error::{PlcError, Result};use serde::{Deserialize, Serialize};use std::fmt;use std::str::FromStr;
/// The prefix for all did:plc identifierspub const DID_PLC_PREFIX: &str = "did:plc:";
/// The length of the identifier portion (24 characters)pub const IDENTIFIER_LENGTH: usize = 24;
/// The total length of a valid did:plc string (32 characters)pub const TOTAL_LENGTH: usize = 32; // "did:plc:" (8) + identifier (24)
/// Represents a validated did:plc identifier.////// A did:plc consists of the prefix "did:plc:" followed by exactly 24/// lowercase base32 characters (using alphabet abcdefghijklmnopqrstuvwxyz234567).////// # Examples////// ```/// use atproto_plc::Did;////// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?;/// assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz");/// # Ok::<(), atproto_plc::PlcError>(())/// ```////// # Format////// The identifier is derived from the SHA-256 hash of the genesis operation,/// base32-encoded and truncated to 24 characters.#[derive(Debug, Clone, PartialEq, Eq, Hash)]pub struct Did { /// The full did:plc:xyz... string full: String, /// The 24-character identifier portion identifier: String,}
impl Did { /// Parse and validate a DID string /// /// # Errors /// /// Returns `PlcError::InvalidDidFormat` if: /// - The string doesn't start with "did:plc:" /// - The total length isn't exactly 32 characters /// - The identifier portion contains invalid base32 characters /// /// # Examples /// /// ``` /// use atproto_plc::Did; /// /// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; /// assert!(did.is_valid()); /// # Ok::<(), atproto_plc::PlcError>(()) /// ``` pub fn parse(s: &str) -> Result<Self> { Self::validate_format(s)?;
let identifier = s[DID_PLC_PREFIX.len()..].to_string();
Ok(Self { full: s.to_string(), identifier, }) }
/// Create a DID from a validated identifier (without the "did:plc:" prefix) /// /// # Errors /// /// Returns `PlcError::InvalidDidFormat` if the identifier is not exactly 24 characters /// or contains invalid base32 characters pub fn from_identifier(identifier: &str) -> Result<Self> { if identifier.len() != IDENTIFIER_LENGTH { return Err(PlcError::InvalidDidFormat(format!( "Identifier must be exactly {} characters, got {}", IDENTIFIER_LENGTH, identifier.len() ))); }
if !is_valid_base32(identifier) { return Err(PlcError::InvalidDidFormat( "Identifier contains invalid base32 characters".to_string(), )); }
Ok(Self { full: format!("{}{}", DID_PLC_PREFIX, identifier), identifier: identifier.to_string(), }) }
/// Get the 24-character identifier portion (without "did:plc:" prefix) /// /// # Examples /// /// ``` /// use atproto_plc::Did; /// /// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; /// assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz"); /// # Ok::<(), atproto_plc::PlcError>(()) /// ``` pub fn identifier(&self) -> &str { &self.identifier }
/// Get the full DID string including "did:plc:" prefix /// /// # Examples /// /// ``` /// use atproto_plc::Did; /// /// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; /// assert_eq!(did.as_str(), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); /// # Ok::<(), atproto_plc::PlcError>(()) /// ``` pub fn as_str(&self) -> &str { &self.full }
/// Check if this DID is valid /// /// Since DIDs can only be constructed through validation, /// this always returns `true` pub fn is_valid(&self) -> bool { true }
/// Validate the format of a DID string without constructing a Did instance /// /// # Errors /// /// Returns `PlcError::InvalidDidFormat` if validation fails fn validate_format(s: &str) -> Result<()> { // Check prefix if !s.starts_with(DID_PLC_PREFIX) { return Err(PlcError::InvalidDidFormat(format!( "DID must start with '{}', got '{}'", DID_PLC_PREFIX, s.chars().take(8).collect::<String>() ))); }
// Check exact length if s.len() != TOTAL_LENGTH { return Err(PlcError::InvalidDidFormat(format!( "DID must be exactly {} characters, got {}", TOTAL_LENGTH, s.len() ))); }
// Extract and validate identifier let identifier = &s[DID_PLC_PREFIX.len()..];
if !is_valid_base32(identifier) { return Err(PlcError::InvalidDidFormat(format!( "Identifier contains invalid base32 characters. Valid alphabet: abcdefghijklmnopqrstuvwxyz234567" ))); }
Ok(()) }}
impl fmt::Display for Did { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.full) }}
impl FromStr for Did { type Err = PlcError;
fn from_str(s: &str) -> Result<Self> { Self::parse(s) }}
impl Serialize for Did { fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> where S: serde::Serializer, { serializer.serialize_str(&self.full) }}
impl<'de> Deserialize<'de> for Did { fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; Self::parse(&s).map_err(serde::de::Error::custom) }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_valid_did() { let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz"); assert_eq!(did.as_str(), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); assert!(did.is_valid()); }
#[test] fn test_invalid_prefix() { assert!(Did::parse("did:web:example.com").is_err()); assert!(Did::parse("DID:PLC:ewvi7nxzyoun6zhxrhs64oiz").is_err()); }
#[test] fn test_invalid_length() { assert!(Did::parse("did:plc:tooshort").is_err()); assert!(Did::parse("did:plc:wayyyyyyyyyyyyyyyyyyyyyyytooooooolong").is_err()); }
#[test] fn test_invalid_characters() { // Contains 0, 1, 8, 9 which are not in base32 alphabet assert!(Did::parse("did:plc:012345678901234567890123").is_err()); // Contains uppercase assert!(Did::parse("did:plc:EWVI7NXZYOUN6ZHXRHS64OIZ").is_err()); }
#[test] fn test_from_identifier() { let did = Did::from_identifier("ewvi7nxzyoun6zhxrhs64oiz").unwrap(); assert_eq!(did.as_str(), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); }
#[test] fn test_display() { let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); assert_eq!(format!("{}", did), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); }
#[test] fn test_serialization() { let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); let json = serde_json::to_string(&did).unwrap(); assert_eq!(json, "\"did:plc:ewvi7nxzyoun6zhxrhs64oiz\"");
let deserialized: Did = serde_json::from_str(&json).unwrap(); assert_eq!(did, deserialized); }}