//! Optimized structure for storing handle resolution results. //! //! This module provides a compact binary representation of handle resolution //! results using bincode serialization. The structure efficiently stores //! the resolution timestamp, DID method type, and the DID payload. use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; /// Errors that can occur during handle resolution result operations #[derive(Debug, Error)] pub enum HandleResolutionError { /// System time error when getting timestamp #[error("error-quickdid-result-1 System time error: {0}")] SystemTime(String), /// Failed to serialize resolution result to binary format #[error("error-quickdid-result-2 Failed to serialize resolution result: {0}")] Serialization(String), /// Failed to deserialize resolution result from binary format #[error("error-quickdid-result-3 Failed to deserialize resolution result: {0}")] Deserialization(String), } /// Represents the type of DID method from a resolution #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(i16)] pub enum DidMethodType { /// Handle was not resolved NotResolved = 0, /// DID uses the web method (did:web:...) Web = 1, /// DID uses the PLC method (did:plc:...) Plc = 2, /// DID uses another method Other = 3, } /// Optimized structure for storing handle resolution results #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct HandleResolutionResult { /// Unix timestamp (seconds since epoch) when the handle was resolved pub timestamp: u64, /// The type of DID method (stored as 4-byte integer) pub method_type: DidMethodType, /// The DID payload (without prefix for web/plc methods) pub payload: String, } impl HandleResolutionResult { /// Create a new resolution result for a successfully resolved handle pub fn success(did: &str) -> Result { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| HandleResolutionError::SystemTime(e.to_string()))? .as_secs(); let (method_type, payload) = Self::parse_did(did); Ok(Self { timestamp, method_type, payload, }) } /// Create a new resolution result for a failed resolution pub fn not_resolved() -> Result { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| HandleResolutionError::SystemTime(e.to_string()))? .as_secs(); Ok(Self { timestamp, method_type: DidMethodType::NotResolved, payload: String::new(), }) } /// Parse a DID string to extract method type and payload fn parse_did(did: &str) -> (DidMethodType, String) { if let Some(stripped) = did.strip_prefix("did:web:") { (DidMethodType::Web, stripped.to_string()) } else if let Some(stripped) = did.strip_prefix("did:plc:") { (DidMethodType::Plc, stripped.to_string()) } else if did.starts_with("did:") { // Other DID method - store the full DID (DidMethodType::Other, did.to_string()) } else { // Not a valid DID format - treat as other (DidMethodType::Other, did.to_string()) } } /// Reconstruct the full DID from the stored data pub fn to_did(&self) -> Option { match self.method_type { DidMethodType::NotResolved => None, DidMethodType::Web => Some(format!("did:web:{}", self.payload)), DidMethodType::Plc => Some(format!("did:plc:{}", self.payload)), DidMethodType::Other => Some(self.payload.clone()), } } /// Serialize the result to bytes using bincode pub fn to_bytes(&self) -> Result, HandleResolutionError> { bincode::serde::encode_to_vec(self, bincode::config::standard()) .map_err(|e| HandleResolutionError::Serialization(e.to_string())) } /// Deserialize from bytes using bincode pub fn from_bytes(bytes: &[u8]) -> Result { bincode::serde::decode_from_slice(bytes, bincode::config::standard()) .map(|(result, _)| result) .map_err(|e| HandleResolutionError::Deserialization(e.to_string())) } } #[cfg(test)] mod tests { use super::*; impl HandleResolutionResult { /// Test-only helper to create a success result without error handling fn success_unchecked(did: &str) -> Self { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs(); let (method_type, payload) = Self::parse_did(did); Self { timestamp, method_type, payload, } } /// Test-only helper to create a not resolved result without error handling fn not_resolved_unchecked() -> Self { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs(); Self { timestamp, method_type: DidMethodType::NotResolved, payload: String::new(), } } /// Test-only helper to create a result with a specific timestamp fn with_timestamp(did: Option<&str>, timestamp: u64) -> Self { match did { Some(did) => { let (method_type, payload) = Self::parse_did(did); Self { timestamp, method_type, payload, } } None => Self { timestamp, method_type: DidMethodType::NotResolved, payload: String::new(), }, } } } #[test] fn test_parse_did_web() { let did = "did:web:example.com"; let result = HandleResolutionResult::success_unchecked(did); assert_eq!(result.method_type, DidMethodType::Web); assert_eq!(result.payload, "example.com"); assert_eq!(result.to_did(), Some(did.to_string())); } #[test] fn test_parse_did_plc() { let did = "did:plc:abcdef123456"; let result = HandleResolutionResult::success_unchecked(did); assert_eq!(result.method_type, DidMethodType::Plc); assert_eq!(result.payload, "abcdef123456"); assert_eq!(result.to_did(), Some(did.to_string())); } #[test] fn test_parse_did_other() { let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; let result = HandleResolutionResult::success_unchecked(did); assert_eq!(result.method_type, DidMethodType::Other); assert_eq!(result.payload, did); assert_eq!(result.to_did(), Some(did.to_string())); } #[test] fn test_not_resolved() { let result = HandleResolutionResult::not_resolved_unchecked(); assert_eq!(result.method_type, DidMethodType::NotResolved); assert_eq!(result.payload, ""); assert_eq!(result.to_did(), None); } #[test] fn test_serialization_deserialization() { // Test did:web let original = HandleResolutionResult::with_timestamp(Some("did:web:example.com:user"), 1234567890); let bytes = original.to_bytes().unwrap(); let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); assert_eq!(original, deserialized); assert_eq!(deserialized.timestamp, 1234567890); assert_eq!(deserialized.method_type, DidMethodType::Web); assert_eq!(deserialized.payload, "example.com:user"); // Test did:plc let original = HandleResolutionResult::with_timestamp( Some("did:plc:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbr"), 9876543210, ); let bytes = original.to_bytes().unwrap(); let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); assert_eq!(original, deserialized); assert_eq!(deserialized.timestamp, 9876543210); assert_eq!(deserialized.method_type, DidMethodType::Plc); assert_eq!( deserialized.payload, "z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbr" ); // Test not resolved let original = HandleResolutionResult::with_timestamp(None, 1111111111); let bytes = original.to_bytes().unwrap(); let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); assert_eq!(original, deserialized); assert_eq!(deserialized.timestamp, 1111111111); assert_eq!(deserialized.method_type, DidMethodType::NotResolved); assert_eq!(deserialized.payload, ""); } #[test] fn test_binary_size() { // Test that serialized size is reasonable let result = HandleResolutionResult::with_timestamp(Some("did:plc:abcdef123456"), 1234567890); let bytes = result.to_bytes().unwrap(); // Should be relatively compact: 8 bytes (u64) + 4 bytes (i32) + string length + metadata assert!( bytes.len() < 100, "Serialized size is too large: {} bytes", bytes.len() ); // Verify the size for not resolved (should be minimal) let not_resolved = HandleResolutionResult::not_resolved_unchecked(); let bytes = not_resolved.to_bytes().unwrap(); assert!( bytes.len() < 50, "Not resolved size is too large: {} bytes", bytes.len() ); } #[test] fn test_did_web_with_port() { let did = "did:web:localhost:3000"; let result = HandleResolutionResult::success_unchecked(did); assert_eq!(result.method_type, DidMethodType::Web); assert_eq!(result.payload, "localhost:3000"); assert_eq!(result.to_did(), Some(did.to_string())); } #[test] fn test_did_web_with_path() { let did = "did:web:example.com:path:to:did"; let result = HandleResolutionResult::success_unchecked(did); assert_eq!(result.method_type, DidMethodType::Web); assert_eq!(result.payload, "example.com:path:to:did"); assert_eq!(result.to_did(), Some(did.to_string())); } #[test] fn test_invalid_did_format() { let did = "not-a-did"; let result = HandleResolutionResult::success_unchecked(did); assert_eq!(result.method_type, DidMethodType::Other); assert_eq!(result.payload, did); assert_eq!(result.to_did(), Some(did.to_string())); } #[test] fn test_round_trip_all_types() { let test_cases = vec![ ("did:web:example.com", DidMethodType::Web, "example.com"), ("did:plc:xyz789", DidMethodType::Plc, "xyz789"), ( "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", DidMethodType::Other, "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", ), ]; for (did, expected_type, expected_payload) in test_cases { let result = HandleResolutionResult::success_unchecked(did); assert_eq!(result.method_type, expected_type); assert_eq!(result.payload, expected_payload); // Test serialization round-trip let bytes = result.to_bytes().unwrap(); let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); assert_eq!(result, deserialized); assert_eq!(deserialized.to_did(), Some(did.to_string())); } } }