//! Encoding utilities for base32, base64url, and DAG-CBOR use crate::error::{PlcError, Result}; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use cid::Cid; use data_encoding::BASE32_NOPAD; use multihash::Multihash; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; /// Base32 alphabet used for did:plc identifiers /// Lowercase, excludes 0,1,8,9 const BASE32_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz234567"; /// Maximum size for an operation in bytes pub const MAX_OPERATION_SIZE: usize = 7500; /// Encode bytes to base32 using the lowercase alphabet /// /// # Examples /// /// ``` /// use atproto_plc::encoding::base32_encode; /// /// let data = b"hello world"; /// let encoded = base32_encode(data); /// assert!(!encoded.is_empty()); /// ``` pub fn base32_encode(data: &[u8]) -> String { BASE32_NOPAD.encode(data).to_lowercase() } /// Decode base32 string to bytes /// /// # Errors /// /// Returns `PlcError::InvalidBase32` if the input contains invalid characters pub fn base32_decode(s: &str) -> Result> { // Validate that all characters are in the allowed alphabet if !s.chars().all(|c| BASE32_ALPHABET.contains(c)) { return Err(PlcError::InvalidBase32(format!( "String contains invalid characters. Allowed: {}", BASE32_ALPHABET ))); } BASE32_NOPAD .decode(s.to_uppercase().as_bytes()) .map_err(|e| PlcError::InvalidBase32(e.to_string())) } /// Encode bytes to base64url without padding /// /// # Examples /// /// ``` /// use atproto_plc::encoding::base64url_encode; /// /// let data = b"hello world"; /// let encoded = base64url_encode(data); /// assert!(!encoded.contains('=')); /// ``` pub fn base64url_encode(data: &[u8]) -> String { URL_SAFE_NO_PAD.encode(data) } /// Decode base64url string to bytes /// /// # Errors /// /// Returns `PlcError::InvalidBase64Url` if the input is not valid base64url pub fn base64url_decode(s: &str) -> Result> { URL_SAFE_NO_PAD .decode(s.as_bytes()) .map_err(|e| PlcError::InvalidBase64Url(e.to_string())) } /// Encode a value to DAG-CBOR format /// /// # Errors /// /// Returns `PlcError::DagCborError` if serialization fails or the result exceeds MAX_OPERATION_SIZE pub fn dag_cbor_encode(value: &T) -> Result> { let bytes = serde_ipld_dagcbor::to_vec(value) .map_err(|e| PlcError::DagCborError(e.to_string()))?; if bytes.len() > MAX_OPERATION_SIZE { return Err(PlcError::OperationTooLarge(bytes.len())); } Ok(bytes) } /// Decode a value from DAG-CBOR format /// /// # Errors /// /// Returns `PlcError::DagCborDecodeError` if deserialization fails pub fn dag_cbor_decode Deserialize<'de>>(data: &[u8]) -> Result { serde_ipld_dagcbor::from_slice(data) .map_err(|e| PlcError::DagCborDecodeError(e.to_string())) } /// Compute the CID (Content Identifier) of data using SHA-256 and dag-cbor codec /// /// The CID is computed as: /// 1. Hash the data with SHA-256 /// 2. Create a multihash with the hash /// 3. Create a CIDv1 with dag-cbor codec /// 4. Encode as base32 /// /// # Examples /// /// ``` /// use atproto_plc::encoding::compute_cid; /// /// let data = b"hello world"; /// let cid = compute_cid(data).unwrap(); /// assert!(cid.starts_with("bafy")); /// ``` pub fn compute_cid(data: &[u8]) -> Result { // Hash the data with SHA-256 let hash_bytes = sha256(data); // Create multihash (0x12 = SHA-256, followed by length and hash) let mut multihash_bytes = Vec::with_capacity(34); // 2 bytes header + 32 bytes hash multihash_bytes.push(0x12); // SHA-256 code multihash_bytes.push(32); // Hash length multihash_bytes.extend_from_slice(&hash_bytes); // Create multihash let multihash = Multihash::from_bytes(&multihash_bytes) .map_err(|e| PlcError::InvalidCid(format!("Failed to create multihash: {:?}", e)))?; // Create CIDv1 with dag-cbor codec (0x71) let cid = Cid::new_v1(0x71, multihash); Ok(cid.to_string()) } /// Hash data with SHA-256 and return the digest pub fn sha256(data: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(data); hasher.finalize().into() } /// Validate that a string is a valid base32 encoding /// /// Returns `true` if all characters are in the allowed alphabet pub fn is_valid_base32(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| BASE32_ALPHABET.contains(c)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_base32_roundtrip() { let data = b"hello world"; let encoded = base32_encode(data); let decoded = base32_decode(&encoded).unwrap(); assert_eq!(data, decoded.as_slice()); } #[test] fn test_base32_invalid_chars() { assert!(base32_decode("0189").is_err()); // Invalid chars: 0, 1, 8, 9 assert!(base32_decode("ABCD").is_err()); // Uppercase not allowed } #[test] fn test_base64url_roundtrip() { let data = b"hello world"; let encoded = base64url_encode(data); let decoded = base64url_decode(&encoded).unwrap(); assert_eq!(data, decoded.as_slice()); assert!(!encoded.contains('=')); } #[test] fn test_is_valid_base32() { assert!(is_valid_base32("abcdefghijklmnopqrstuvwxyz234567")); assert!(!is_valid_base32("0189")); assert!(!is_valid_base32("ABCD")); assert!(!is_valid_base32("")); } #[test] fn test_sha256() { let data = b"hello world"; let hash = sha256(data); assert_eq!(hash.len(), 32); } #[test] fn test_compute_cid() { let data = b"hello world"; let cid = compute_cid(data).unwrap(); assert!(cid.starts_with("b")); // CIDv1 starts with 'b' in base32 } }