Rust and WASM did-method-plc tools and structures
1//! DID (Decentralized Identifier) types and validation for did:plc 2 3use crate::encoding::is_valid_base32; 4use crate::error::{PlcError, Result}; 5use serde::{Deserialize, Serialize}; 6use std::fmt; 7use std::str::FromStr; 8 9/// The prefix for all did:plc identifiers 10pub const DID_PLC_PREFIX: &str = "did:plc:"; 11 12/// The length of the identifier portion (24 characters) 13pub const IDENTIFIER_LENGTH: usize = 24; 14 15/// The total length of a valid did:plc string (32 characters) 16pub const TOTAL_LENGTH: usize = 32; // "did:plc:" (8) + identifier (24) 17 18/// Represents a validated did:plc identifier. 19/// 20/// A did:plc consists of the prefix "did:plc:" followed by exactly 24 21/// lowercase base32 characters (using alphabet abcdefghijklmnopqrstuvwxyz234567). 22/// 23/// # Examples 24/// 25/// ``` 26/// use atproto_plc::Did; 27/// 28/// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; 29/// assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz"); 30/// # Ok::<(), atproto_plc::PlcError>(()) 31/// ``` 32/// 33/// # Format 34/// 35/// The identifier is derived from the SHA-256 hash of the genesis operation, 36/// base32-encoded and truncated to 24 characters. 37#[derive(Debug, Clone, PartialEq, Eq, Hash)] 38pub struct Did { 39 /// The full did:plc:xyz... string 40 full: String, 41 /// The 24-character identifier portion 42 identifier: String, 43} 44 45impl Did { 46 /// Parse and validate a DID string 47 /// 48 /// # Errors 49 /// 50 /// Returns `PlcError::InvalidDidFormat` if: 51 /// - The string doesn't start with "did:plc:" 52 /// - The total length isn't exactly 32 characters 53 /// - The identifier portion contains invalid base32 characters 54 /// 55 /// # Examples 56 /// 57 /// ``` 58 /// use atproto_plc::Did; 59 /// 60 /// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; 61 /// assert!(did.is_valid()); 62 /// # Ok::<(), atproto_plc::PlcError>(()) 63 /// ``` 64 pub fn parse(s: &str) -> Result<Self> { 65 Self::validate_format(s)?; 66 67 let identifier = s[DID_PLC_PREFIX.len()..].to_string(); 68 69 Ok(Self { 70 full: s.to_string(), 71 identifier, 72 }) 73 } 74 75 /// Create a DID from a validated identifier (without the "did:plc:" prefix) 76 /// 77 /// # Errors 78 /// 79 /// Returns `PlcError::InvalidDidFormat` if the identifier is not exactly 24 characters 80 /// or contains invalid base32 characters 81 pub fn from_identifier(identifier: &str) -> Result<Self> { 82 if identifier.len() != IDENTIFIER_LENGTH { 83 return Err(PlcError::InvalidDidFormat(format!( 84 "Identifier must be exactly {} characters, got {}", 85 IDENTIFIER_LENGTH, 86 identifier.len() 87 ))); 88 } 89 90 if !is_valid_base32(identifier) { 91 return Err(PlcError::InvalidDidFormat( 92 "Identifier contains invalid base32 characters".to_string(), 93 )); 94 } 95 96 Ok(Self { 97 full: format!("{}{}", DID_PLC_PREFIX, identifier), 98 identifier: identifier.to_string(), 99 }) 100 } 101 102 /// Get the 24-character identifier portion (without "did:plc:" prefix) 103 /// 104 /// # Examples 105 /// 106 /// ``` 107 /// use atproto_plc::Did; 108 /// 109 /// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; 110 /// assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz"); 111 /// # Ok::<(), atproto_plc::PlcError>(()) 112 /// ``` 113 pub fn identifier(&self) -> &str { 114 &self.identifier 115 } 116 117 /// Get the full DID string including "did:plc:" prefix 118 /// 119 /// # Examples 120 /// 121 /// ``` 122 /// use atproto_plc::Did; 123 /// 124 /// let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")?; 125 /// assert_eq!(did.as_str(), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); 126 /// # Ok::<(), atproto_plc::PlcError>(()) 127 /// ``` 128 pub fn as_str(&self) -> &str { 129 &self.full 130 } 131 132 /// Check if this DID is valid 133 /// 134 /// Since DIDs can only be constructed through validation, 135 /// this always returns `true` 136 pub fn is_valid(&self) -> bool { 137 true 138 } 139 140 /// Validate the format of a DID string without constructing a Did instance 141 /// 142 /// # Errors 143 /// 144 /// Returns `PlcError::InvalidDidFormat` if validation fails 145 fn validate_format(s: &str) -> Result<()> { 146 // Check prefix 147 if !s.starts_with(DID_PLC_PREFIX) { 148 return Err(PlcError::InvalidDidFormat(format!( 149 "DID must start with '{}', got '{}'", 150 DID_PLC_PREFIX, 151 s.chars().take(8).collect::<String>() 152 ))); 153 } 154 155 // Check exact length 156 if s.len() != TOTAL_LENGTH { 157 return Err(PlcError::InvalidDidFormat(format!( 158 "DID must be exactly {} characters, got {}", 159 TOTAL_LENGTH, 160 s.len() 161 ))); 162 } 163 164 // Extract and validate identifier 165 let identifier = &s[DID_PLC_PREFIX.len()..]; 166 167 if !is_valid_base32(identifier) { 168 return Err(PlcError::InvalidDidFormat(format!( 169 "Identifier contains invalid base32 characters. Valid alphabet: abcdefghijklmnopqrstuvwxyz234567" 170 ))); 171 } 172 173 Ok(()) 174 } 175} 176 177impl fmt::Display for Did { 178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 179 write!(f, "{}", self.full) 180 } 181} 182 183impl FromStr for Did { 184 type Err = PlcError; 185 186 fn from_str(s: &str) -> Result<Self> { 187 Self::parse(s) 188 } 189} 190 191impl Serialize for Did { 192 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> 193 where 194 S: serde::Serializer, 195 { 196 serializer.serialize_str(&self.full) 197 } 198} 199 200impl<'de> Deserialize<'de> for Did { 201 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> 202 where 203 D: serde::Deserializer<'de>, 204 { 205 let s = String::deserialize(deserializer)?; 206 Self::parse(&s).map_err(serde::de::Error::custom) 207 } 208} 209 210#[cfg(test)] 211mod tests { 212 use super::*; 213 214 #[test] 215 fn test_valid_did() { 216 let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); 217 assert_eq!(did.identifier(), "ewvi7nxzyoun6zhxrhs64oiz"); 218 assert_eq!(did.as_str(), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); 219 assert!(did.is_valid()); 220 } 221 222 #[test] 223 fn test_invalid_prefix() { 224 assert!(Did::parse("did:web:example.com").is_err()); 225 assert!(Did::parse("DID:PLC:ewvi7nxzyoun6zhxrhs64oiz").is_err()); 226 } 227 228 #[test] 229 fn test_invalid_length() { 230 assert!(Did::parse("did:plc:tooshort").is_err()); 231 assert!(Did::parse("did:plc:wayyyyyyyyyyyyyyyyyyyyyyytooooooolong").is_err()); 232 } 233 234 #[test] 235 fn test_invalid_characters() { 236 // Contains 0, 1, 8, 9 which are not in base32 alphabet 237 assert!(Did::parse("did:plc:012345678901234567890123").is_err()); 238 // Contains uppercase 239 assert!(Did::parse("did:plc:EWVI7NXZYOUN6ZHXRHS64OIZ").is_err()); 240 } 241 242 #[test] 243 fn test_from_identifier() { 244 let did = Did::from_identifier("ewvi7nxzyoun6zhxrhs64oiz").unwrap(); 245 assert_eq!(did.as_str(), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); 246 } 247 248 #[test] 249 fn test_display() { 250 let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); 251 assert_eq!(format!("{}", did), "did:plc:ewvi7nxzyoun6zhxrhs64oiz"); 252 } 253 254 #[test] 255 fn test_serialization() { 256 let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); 257 let json = serde_json::to_string(&did).unwrap(); 258 assert_eq!(json, "\"did:plc:ewvi7nxzyoun6zhxrhs64oiz\""); 259 260 let deserialized: Did = serde_json::from_str(&json).unwrap(); 261 assert_eq!(did, deserialized); 262 } 263}