Rust AppView - highly experimental!
at experiments 699 lines 25 kB view raw
1/// CID optimization utilities for database storage 2/// 3/// AT Protocol uses two types of CIDs, both CIDv1 with SHA-256: 4/// 5/// 1. **Blob CIDs** (images, videos, avatars, banners): 6/// - 0x01: CIDv1 version 7/// - 0x55: Raw codec (multicodec for raw binary data) 8/// - 0x1220: SHA-256 multihash header (0x12=SHA256, 0x20=32 bytes) 9/// - 32 bytes: SHA-256 digest 10/// - String format: "bafkrei..." (base32-encoded) 11/// 12/// 2. **Record CIDs** (posts, profiles, lists, etc.): 13/// - 0x01: CIDv1 version 14/// - 0x71: DAG-CBOR codec (multicodec for CBOR-encoded DAG nodes) 15/// - 0x1220: SHA-256 multihash header (0x12=SHA256, 0x20=32 bytes) 16/// - 32 bytes: SHA-256 digest 17/// - String format: "bafyrei..." (base32-encoded) 18/// 19/// Total: 36 bytes for both types 20/// 21/// We store only the 32-byte digest in the database, stripping the 4-byte 22/// prefix. When reconstructing, we use the appropriate header based on context 23/// (blobs use raw codec, records use dag-cbor codec). 24/// 25/// Reference: https://atproto.com/specs/data-model 26/// CIDv1 header for blob CIDs (images, videos, etc.) 27/// 0x01 (CIDv1) + 0x55 (raw codec) + 0x1220 (SHA-256 multihash header) 28pub const BLOB_CID_HEADER: [u8; 4] = [0x01, 0x55, 0x12, 0x20]; 29 30/// CIDv1 header for record CIDs (posts, profiles, lists, etc.) 31/// 0x01 (CIDv1) + 0x71 (dag-cbor codec) + 0x1220 (SHA-256 multihash header) 32pub const RECORD_CID_HEADER: [u8; 4] = [0x01, 0x71, 0x12, 0x20]; 33 34/// Strip the CIDv1 header from a binary CID, returning only the 32-byte digest 35/// 36/// Accepts both blob CIDs (0x55 raw codec) and record CIDs (0x71 dag-cbor codec). 37/// 38/// # Arguments 39/// * `cid_bytes` - Full CID bytes (must be 36 bytes with valid CIDv1 header) 40/// 41/// # Returns 42/// * `Some(&[u8])` - Slice to the 32-byte digest 43/// * `None` - If the CID is not 36 bytes or doesn't have a valid AT Protocol header 44#[inline] 45pub fn cid_to_digest(cid_bytes: &[u8]) -> Option<&[u8]> { 46 if cid_bytes.len() == 36 { 47 // Check if it's a valid AT Protocol CID (blob or record) 48 if cid_bytes.starts_with(&BLOB_CID_HEADER) || cid_bytes.starts_with(&RECORD_CID_HEADER) { 49 return Some(&cid_bytes[4..]); 50 } 51 } 52 None 53} 54 55/// Strip the CIDv1 header from a binary CID, returning owned 32-byte digest 56/// 57/// # Arguments 58/// * `cid_bytes` - Full CID bytes (must be 36 bytes with correct header) 59/// 60/// # Returns 61/// * `Some(Vec<u8>)` - The 32-byte digest 62/// * `None` - If the CID is invalid 63#[inline] 64pub fn cid_to_digest_owned(cid_bytes: &[u8]) -> Option<Vec<u8>> { 65 cid_to_digest(cid_bytes).map(|d| d.to_vec()) 66} 67 68/// Reconstruct a full blob CID from a 32-byte digest 69/// 70/// Creates a CID with raw codec (0x55) suitable for blobs (images, videos, etc.). 71/// Produces CIDs that start with "bafkrei..." when base32-encoded. 72/// 73/// # Arguments 74/// * `digest` - The 32-byte SHA-256 digest 75/// 76/// # Returns 77/// * `Some(Vec<u8>)` - The full 36-byte blob CID 78/// * `None` - If the digest is not 32 bytes 79#[inline] 80pub fn digest_to_blob_cid(digest: &[u8]) -> Option<Vec<u8>> { 81 if digest.len() == 32 { 82 let mut cid = Vec::with_capacity(36); 83 cid.extend_from_slice(&BLOB_CID_HEADER); 84 cid.extend_from_slice(digest); 85 Some(cid) 86 } else { 87 None 88 } 89} 90 91/// Reconstruct a full record CID from a 32-byte digest 92/// 93/// Creates a CID with dag-cbor codec (0x71) suitable for records (posts, profiles, etc.). 94/// Produces CIDs that start with "bafyrei..." when base32-encoded. 95/// 96/// # Arguments 97/// * `digest` - The 32-byte SHA-256 digest 98/// 99/// # Returns 100/// * `Some(Vec<u8>)` - The full 36-byte record CID 101/// * `None` - If the digest is not 32 bytes 102#[inline] 103pub fn digest_to_record_cid(digest: &[u8]) -> Option<Vec<u8>> { 104 if digest.len() == 32 { 105 let mut cid = Vec::with_capacity(36); 106 cid.extend_from_slice(&RECORD_CID_HEADER); 107 cid.extend_from_slice(digest); 108 Some(cid) 109 } else { 110 None 111 } 112} 113/// Convert a 32-byte digest to a base32 blob CID string 114/// 115/// Creates a CID string with raw codec (0x55) suitable for blobs. 116/// Produces strings starting with "bafkrei...". 117/// 118/// # Arguments 119/// * `digest` - The 32-byte SHA-256 digest 120/// 121/// # Returns 122/// * `Some(String)` - Base32-encoded blob CID string (e.g., "bafkrei...") 123/// * `None` - If the digest is not 32 bytes 124pub fn digest_to_blob_cid_string(digest: &[u8]) -> Option<String> { 125 let full_cid = digest_to_blob_cid(digest)?; 126 // Use multibase to encode directly without parsing 127 // CIDv1 uses base32 (lowercase) encoding 128 Some(multibase::encode(multibase::Base::Base32Lower, &full_cid)) 129} 130 131/// Convert a 32-byte digest to a base32 record CID string 132/// 133/// Creates a CID string with dag-cbor codec (0x71) suitable for records. 134/// Produces strings starting with "bafyrei...". 135/// 136/// # Arguments 137/// * `digest` - The 32-byte SHA-256 digest 138/// 139/// # Returns 140/// * `Some(String)` - Base32-encoded record CID string (e.g., "bafyrei...") 141/// * `None` - If the digest is not 32 bytes 142pub fn digest_to_record_cid_string(digest: &[u8]) -> Option<String> { 143 let full_cid = digest_to_record_cid(digest)?; 144 // Use multibase to encode directly without parsing 145 // CIDv1 uses base32 (lowercase) encoding 146 Some(multibase::encode(multibase::Base::Base32Lower, &full_cid)) 147} 148 149/// Generate a deterministic CID digest for any record type 150/// 151/// Computes SHA-256("parakeet:{record_type}:{actor_id}:{rkey}") 152#[inline] 153fn generate_cid_digest(record_type: &str, actor_id: i32, rkey: i64) -> Vec<u8> { 154 use sha2::{Digest, Sha256}; 155 let data = format!("parakeet:{}:{}:{}", record_type, actor_id, rkey); 156 Sha256::digest(data.as_bytes()).to_vec() 157} 158 159/// Generate a deterministic digest for a like record 160pub fn generate_like_cid_digest(actor_id: i32, rkey: i64) -> Vec<u8> { 161 generate_cid_digest("like", actor_id, rkey) 162} 163 164/// Generate a synthetic CID string for a like record (returns "bafyrei...") 165pub fn like_cid_string(actor_id: i32, rkey: i64) -> String { 166 let digest = generate_like_cid_digest(actor_id, rkey); 167 digest_to_record_cid_string(&digest) 168 .unwrap_or_else(|| String::from("bafyrei_invalid_cid")) 169} 170 171/// Generate a deterministic digest for a repost record 172pub fn generate_repost_cid_digest(actor_id: i32, rkey: i64) -> Vec<u8> { 173 generate_cid_digest("repost", actor_id, rkey) 174} 175 176/// Generate a synthetic CID string for a repost record (returns "bafyrei...") 177pub fn repost_cid_string(actor_id: i32, rkey: i64) -> String { 178 let digest = generate_repost_cid_digest(actor_id, rkey); 179 digest_to_record_cid_string(&digest) 180 .unwrap_or_else(|| String::from("bafyrei_invalid_cid")) 181} 182 183/// Generate a deterministic digest for a follow record 184pub fn generate_follow_cid_digest(actor_id: i32, rkey: i64) -> Vec<u8> { 185 generate_cid_digest("follow", actor_id, rkey) 186} 187 188/// Generate a synthetic CID string for a follow record (returns "bafyrei...") 189pub fn follow_cid_string(actor_id: i32, rkey: i64) -> String { 190 let digest = generate_follow_cid_digest(actor_id, rkey); 191 digest_to_record_cid_string(&digest) 192 .unwrap_or_else(|| String::from("bafyrei_invalid_cid")) 193} 194 195/// Generate a deterministic digest for a block record 196pub fn generate_block_cid_digest(actor_id: i32, rkey: i64) -> Vec<u8> { 197 generate_cid_digest("block", actor_id, rkey) 198} 199 200/// Generate a synthetic CID string for a block record (returns "bafyrei...") 201pub fn block_cid_string(actor_id: i32, rkey: i64) -> String { 202 let digest = generate_block_cid_digest(actor_id, rkey); 203 digest_to_record_cid_string(&digest) 204 .unwrap_or_else(|| String::from("bafyrei_invalid_cid")) 205} 206 207/// Generate a deterministic digest for a post record 208/// 209/// Note: This is a synthetic CID based on (actor_id, rkey) for AppView purposes. 210pub fn generate_post_cid_digest(actor_id: i32, rkey: i64) -> Vec<u8> { 211 generate_cid_digest("post", actor_id, rkey) 212} 213 214/// Generate a synthetic CID string for a post record (returns "bafyrei...") 215pub fn post_cid_string(actor_id: i32, rkey: i64) -> String { 216 let digest = generate_post_cid_digest(actor_id, rkey); 217 digest_to_record_cid_string(&digest) 218 .unwrap_or_else(|| String::from("bafyrei_invalid_cid")) 219} 220 221#[cfg(test)] 222mod tests { 223 use super::*; 224 225 #[test] 226 fn test_header_constants() { 227 assert_eq!(BLOB_CID_HEADER, [0x01, 0x55, 0x12, 0x20]); 228 assert_eq!(RECORD_CID_HEADER, [0x01, 0x71, 0x12, 0x20]); 229 } 230 231 #[test] 232 fn test_cid_to_digest_blob() { 233 // Test with blob CID (0x55 raw codec) 234 let mut blob_cid = BLOB_CID_HEADER.to_vec(); 235 blob_cid.extend(vec![0xab; 32]); 236 237 let digest = cid_to_digest(&blob_cid).unwrap(); 238 assert_eq!(digest.len(), 32); 239 assert_eq!(digest, &vec![0xab; 32][..]); 240 } 241 242 #[test] 243 fn test_cid_to_digest_record() { 244 // Test with record CID (0x71 dag-cbor codec) 245 let mut record_cid = RECORD_CID_HEADER.to_vec(); 246 record_cid.extend(vec![0xab; 32]); 247 248 let digest = cid_to_digest(&record_cid).unwrap(); 249 assert_eq!(digest.len(), 32); 250 assert_eq!(digest, &vec![0xab; 32][..]); 251 } 252 253 #[test] 254 fn test_cid_to_digest_owned_both_types() { 255 // Test with blob CID 256 let mut blob_cid = BLOB_CID_HEADER.to_vec(); 257 blob_cid.extend(vec![0xcd; 32]); 258 let digest = cid_to_digest_owned(&blob_cid).unwrap(); 259 assert_eq!(digest, vec![0xcd; 32]); 260 261 // Test with record CID 262 let mut record_cid = RECORD_CID_HEADER.to_vec(); 263 record_cid.extend(vec![0xef; 32]); 264 let digest = cid_to_digest_owned(&record_cid).unwrap(); 265 assert_eq!(digest, vec![0xef; 32]); 266 } 267 268 #[test] 269 fn test_digest_to_blob_cid() { 270 let digest = vec![0xab; 32]; 271 272 let cid = digest_to_blob_cid(&digest).unwrap(); 273 assert_eq!(cid.len(), 36); 274 assert_eq!(&cid[0..4], &BLOB_CID_HEADER); 275 assert_eq!(&cid[4..], &digest[..]); 276 } 277 278 #[test] 279 fn test_digest_to_record_cid() { 280 let digest = vec![0xcd; 32]; 281 282 let cid = digest_to_record_cid(&digest).unwrap(); 283 assert_eq!(cid.len(), 36); 284 assert_eq!(&cid[0..4], &RECORD_CID_HEADER); 285 assert_eq!(&cid[4..], &digest[..]); 286 } 287 288 #[test] 289 fn test_roundtrip_blob() { 290 let mut blob_cid = BLOB_CID_HEADER.to_vec(); 291 blob_cid.extend(vec![0xef; 32]); 292 293 let digest = cid_to_digest(&blob_cid).unwrap(); 294 let reconstructed = digest_to_blob_cid(digest).unwrap(); 295 296 assert_eq!(reconstructed, blob_cid); 297 } 298 299 #[test] 300 fn test_roundtrip_record() { 301 let mut record_cid = RECORD_CID_HEADER.to_vec(); 302 record_cid.extend(vec![0x12; 32]); 303 304 let digest = cid_to_digest(&record_cid).unwrap(); 305 let reconstructed = digest_to_record_cid(digest).unwrap(); 306 307 assert_eq!(reconstructed, record_cid); 308 } 309 310 #[test] 311 fn test_invalid_cid_length() { 312 let short_cid = vec![0x01, 0x71, 0x12]; 313 assert!(cid_to_digest(&short_cid).is_none()); 314 315 let long_cid = vec![0x01; 100]; 316 assert!(cid_to_digest(&long_cid).is_none()); 317 } 318 319 #[test] 320 fn test_invalid_cid_header() { 321 let mut bad_cid = vec![0x00, 0x00, 0x00, 0x00]; // Wrong header 322 bad_cid.extend(vec![0xab; 32]); 323 324 assert!(cid_to_digest(&bad_cid).is_none()); 325 } 326 327 #[test] 328 fn test_invalid_digest_length() { 329 let short_digest = vec![0xab; 16]; 330 assert!(digest_to_record_cid(&short_digest).is_none()); 331 assert!(digest_to_blob_cid(&short_digest).is_none()); 332 333 let long_digest = vec![0xab; 64]; 334 assert!(digest_to_record_cid(&long_digest).is_none()); 335 assert!(digest_to_blob_cid(&long_digest).is_none()); 336 } 337 338 #[test] 339 fn test_digest_to_blob_cid_string_matches_cid_crate() { 340 let digest = vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 341 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 342 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 343 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x07, 0x18]; 344 345 let our_result = digest_to_blob_cid_string(&digest).unwrap(); 346 347 let full_cid = digest_to_blob_cid(&digest).unwrap(); 348 let cid_obj = cid::Cid::try_from(full_cid.as_slice()).unwrap(); 349 let cid_result = cid_obj.to_string(); 350 351 assert_eq!(our_result, cid_result); 352 assert!(our_result.starts_with("bafkrei"), "Blob CIDs should start with bafkrei"); 353 } 354 355 #[test] 356 fn test_digest_to_record_cid_string_matches_cid_crate() { 357 let digest = vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 358 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 359 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 360 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x07, 0x18]; 361 362 let our_result = digest_to_record_cid_string(&digest).unwrap(); 363 364 let full_cid = digest_to_record_cid(&digest).unwrap(); 365 let cid_obj = cid::Cid::try_from(full_cid.as_slice()).unwrap(); 366 let cid_result = cid_obj.to_string(); 367 368 assert_eq!(our_result, cid_result); 369 assert!(our_result.starts_with("bafyrei"), "Record CIDs should start with bafyrei"); 370 } 371 372 #[test] 373 fn test_blob_cid_string_real_example() { 374 let digest = vec![ 375 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 376 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 377 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 378 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, 379 ]; 380 381 let result = digest_to_blob_cid_string(&digest).unwrap(); 382 383 assert!(result.starts_with('b'), "Should use base32 encoding"); 384 assert!(result.len() > 40, "Base32-encoded CID should be reasonably long"); 385 386 let parsed = cid::Cid::try_from(result.as_str()).unwrap(); 387 assert_eq!(parsed.version(), cid::Version::V1); 388 assert_eq!(parsed.codec(), 0x55); // raw codec for blobs 389 } 390 391 #[test] 392 fn test_record_cid_string_real_example() { 393 let digest = vec![ 394 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 395 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 396 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 397 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, 398 ]; 399 400 let result = digest_to_record_cid_string(&digest).unwrap(); 401 402 assert!(result.starts_with('b'), "Should use base32 encoding"); 403 assert!(result.len() > 40, "Base32-encoded CID should be reasonably long"); 404 405 let parsed = cid::Cid::try_from(result.as_str()).unwrap(); 406 assert_eq!(parsed.version(), cid::Version::V1); 407 assert_eq!(parsed.codec(), 0x71); // dag-cbor codec for records 408 } 409 410 #[test] 411 fn test_roundtrip_blob_with_string() { 412 let original_digest = vec![0x42; 32]; 413 414 let cid_string = digest_to_blob_cid_string(&original_digest).unwrap(); 415 let parsed_cid = cid::Cid::try_from(cid_string.as_str()).unwrap(); 416 let cid_bytes = parsed_cid.to_bytes(); 417 let recovered_digest = cid_to_digest(&cid_bytes).unwrap(); 418 419 assert_eq!(recovered_digest, &original_digest[..]); 420 assert!(cid_string.starts_with("bafkrei")); 421 } 422 423 #[test] 424 fn test_roundtrip_record_with_string() { 425 let original_digest = vec![0x99; 32]; 426 427 let cid_string = digest_to_record_cid_string(&original_digest).unwrap(); 428 let parsed_cid = cid::Cid::try_from(cid_string.as_str()).unwrap(); 429 let cid_bytes = parsed_cid.to_bytes(); 430 let recovered_digest = cid_to_digest(&cid_bytes).unwrap(); 431 432 assert_eq!(recovered_digest, &original_digest[..]); 433 assert!(cid_string.starts_with("bafyrei")); 434 } 435 436 #[test] 437 fn test_synthetic_cid_determinism() { 438 // Synthetic CIDs must be deterministic 439 assert_eq!( 440 like_cid_string(123, 456), 441 like_cid_string(123, 456), 442 "Like CIDs must be deterministic" 443 ); 444 assert_eq!( 445 repost_cid_string(123, 456), 446 repost_cid_string(123, 456), 447 "Repost CIDs must be deterministic" 448 ); 449 assert_eq!( 450 follow_cid_string(123, 456), 451 follow_cid_string(123, 456), 452 "Follow CIDs must be deterministic" 453 ); 454 assert_eq!( 455 block_cid_string(123, 456), 456 block_cid_string(123, 456), 457 "Block CIDs must be deterministic" 458 ); 459 assert_eq!( 460 post_cid_string(123, 456), 461 post_cid_string(123, 456), 462 "Post CIDs must be deterministic" 463 ); 464 } 465 466 #[test] 467 fn test_synthetic_cid_uniqueness() { 468 // Different records must produce different CIDs 469 let like_cid = like_cid_string(123, 456); 470 let repost_cid = repost_cid_string(123, 456); 471 let follow_cid = follow_cid_string(123, 456); 472 let block_cid = block_cid_string(123, 456); 473 let post_cid = post_cid_string(123, 456); 474 475 assert_ne!(like_cid, repost_cid, "Like and repost CIDs must differ"); 476 assert_ne!(like_cid, follow_cid, "Like and follow CIDs must differ"); 477 assert_ne!(like_cid, block_cid, "Like and block CIDs must differ"); 478 assert_ne!(like_cid, post_cid, "Like and post CIDs must differ"); 479 assert_ne!(repost_cid, follow_cid, "Repost and follow CIDs must differ"); 480 assert_ne!(repost_cid, block_cid, "Repost and block CIDs must differ"); 481 assert_ne!(repost_cid, post_cid, "Repost and post CIDs must differ"); 482 483 // Different actors/rkeys must produce different CIDs 484 assert_ne!(like_cid_string(123, 456), like_cid_string(124, 456)); 485 assert_ne!(like_cid_string(123, 456), like_cid_string(123, 457)); 486 assert_ne!(repost_cid_string(123, 456), repost_cid_string(124, 456)); 487 assert_ne!(post_cid_string(123, 456), post_cid_string(123, 457)); 488 } 489 490 #[test] 491 fn test_synthetic_cid_format() { 492 // All synthetic CIDs must be valid AT Protocol record CIDs 493 assert!(like_cid_string(1, 1).starts_with("bafyrei"), "Must use record CID format"); 494 assert!(repost_cid_string(1, 1).starts_with("bafyrei")); 495 assert!(follow_cid_string(1, 1).starts_with("bafyrei")); 496 assert!(block_cid_string(1, 1).starts_with("bafyrei")); 497 assert!(post_cid_string(1, 1).starts_with("bafyrei")); 498 499 // Verify they're parseable as valid CIDs 500 let like = like_cid_string(42, 9999); 501 let parsed = cid::Cid::try_from(like.as_str()).unwrap(); 502 assert_eq!(parsed.version(), cid::Version::V1); 503 assert_eq!(parsed.codec(), 0x71); // dag-cbor codec for records 504 } 505} 506 507// ============================================================================ 508// Jacquard Type Conversions 509// ============================================================================ 510// 511// This section provides conversion utilities between jacquard's borrowed types 512// and parakeet's storage format. It preserves the existing CID optimization 513// where we store only 32-byte digests instead of full 36-byte CIDs. 514 515use jacquard_common::types::blob::Blob; 516use jacquard_common::types::cid::Cid as JacquardCid; 517use jacquard_common::IntoStatic; 518 519/// Storage format for Blob data 520#[derive(Debug, Clone)] 521pub struct BlobStorage { 522 pub mime_type: String, 523 pub cid: Vec<u8>, // 32-byte digest 524 pub size: i32, 525} 526 527/// Storage format for StrongRef data 528#[derive(Debug, Clone)] 529pub struct StrongRefStorage { 530 pub uri: String, 531 pub cid: Vec<u8>, // 32-byte digest 532} 533 534/// Convert jacquard Blob to storage format 535/// 536/// Extracts the 32-byte digest from the CID for database storage 537pub fn blob_to_storage(blob: &Blob) -> Option<BlobStorage> { 538 // Convert jacquard CID to cid::Cid to get bytes 539 let cid = blob.cid().as_str().parse::<cid::Cid>().ok()?; 540 let cid_bytes = cid.to_bytes(); 541 let digest = cid_to_digest_owned(&cid_bytes)?; 542 543 Some(BlobStorage { 544 mime_type: blob.mime_type.to_string(), 545 cid: digest, 546 size: blob.size as i32, 547 }) 548} 549 550/// Convert jacquard StrongRef to storage format 551/// 552/// Extracts the 32-byte digest from the CID for database storage 553pub fn strongref_to_storage(sr: &jacquard_api::com_atproto::repo::strong_ref::StrongRef) -> Option<StrongRefStorage> { 554 // Convert jacquard CID string to cid::Cid to get bytes 555 let cid = sr.cid.parse::<cid::Cid>().ok()?; 556 let cid_bytes = cid.to_bytes(); 557 let digest = cid_to_digest_owned(&cid_bytes)?; 558 559 Some(StrongRefStorage { 560 uri: sr.uri.to_string(), 561 cid: digest, 562 }) 563} 564 565/// Convert jacquard CID to 32-byte digest for storage 566pub fn jacquard_cid_to_digest(cid: &JacquardCid) -> Option<Vec<u8>> { 567 // Convert jacquard CID to cid::Cid first 568 let parsed_cid = cid.as_str().parse::<cid::Cid>().ok()?; 569 let cid_bytes = parsed_cid.to_bytes(); 570 cid_to_digest_owned(&cid_bytes) 571} 572 573/// Reconstruct a jacquard CID from a 32-byte digest 574/// 575/// # Arguments 576/// * `digest` - The 32-byte SHA-256 digest 577/// * `is_blob` - If true, creates a blob CID (raw codec), otherwise record CID (dag-cbor) 578pub fn digest_to_jacquard_cid(digest: &[u8], is_blob: bool) -> Option<JacquardCid<'static>> { 579 if digest.len() != 32 { 580 return None; 581 } 582 583 let mut cid_bytes = Vec::with_capacity(36); 584 if is_blob { 585 cid_bytes.extend_from_slice(&BLOB_CID_HEADER); 586 } else { 587 cid_bytes.extend_from_slice(&RECORD_CID_HEADER); 588 } 589 cid_bytes.extend_from_slice(digest); 590 591 // Convert to cid::Cid first, then to string for jacquard 592 let cid = cid::Cid::try_from(cid_bytes).ok()?; 593 let cid_str = cid.to_string(); 594 // Use the proper constructor for JacquardCid 595 Some(JacquardCid::from(cid_str)) 596} 597 598/// Extract parts from a jacquard StrongRef 599/// 600/// Returns (uri, cid_string) tuple 601pub fn strongref_to_parts(sr: Option<&jacquard_api::com_atproto::repo::strong_ref::StrongRef>) -> (Option<String>, Option<String>) { 602 match sr { 603 Some(sr) => ( 604 Some(sr.uri.to_string()), 605 Some(sr.cid.to_string()), 606 ), 607 None => (None, None), 608 } 609} 610 611/// Extract parts from a jacquard StrongRef with digest 612/// 613/// Returns (uri, cid_digest_bytes) tuple 614pub fn strongref_to_parts_with_digest(sr: Option<&jacquard_api::com_atproto::repo::strong_ref::StrongRef>) -> (Option<String>, Option<Vec<u8>>) { 615 match sr { 616 Some(sr) => { 617 // Parse CID and extract digest 618 let cid = sr.cid.parse::<cid::Cid>().ok(); 619 let digest = cid.and_then(|c| { 620 let bytes = c.to_bytes(); 621 cid_to_digest_owned(&bytes) 622 }); 623 (Some(sr.uri.to_string()), digest) 624 }, 625 None => (None, None), 626 } 627} 628 629/// Extract CID string from a jacquard Blob 630pub fn blob_ref(blob: Option<&Blob>) -> Option<String> { 631 blob.map(|b| b.cid().to_string()) 632} 633 634/// Extract CID digest bytes from a jacquard Blob 635pub fn blob_to_cid_bytes(blob: Option<&Blob>) -> Option<Vec<u8>> { 636 blob.and_then(|b| { 637 // Convert jacquard CID to cid::Cid to get bytes 638 let cid = b.cid().as_str().parse::<cid::Cid>().ok()?; 639 let cid_bytes = cid.to_bytes(); 640 cid_to_digest_owned(&cid_bytes) 641 }) 642} 643 644/// Convert storage format back to jacquard Blob 645/// 646/// Reconstructs the full CID from the stored digest 647pub fn storage_to_blob(storage: &BlobStorage) -> Option<Blob<'static>> { 648 // Reconstruct the full CID from digest 649 let cid = digest_to_jacquard_cid(&storage.cid, true)?; 650 651 // Create a static blob using JSON deserialization 652 // We need to create a JSON string and deserialize from it to get the proper lifetime 653 let blob_json_str = serde_json::json!({ 654 "$type": "blob", 655 "ref": {"$link": cid.as_str()}, 656 "mimeType": &storage.mime_type, 657 "size": storage.size 658 }).to_string(); 659 660 // Deserialize from string to get 'static lifetime 661 serde_json::from_str(&blob_json_str).ok().map(|b: Blob<'_>| b.into_static()) 662} 663 664/// Convert storage format back to jacquard StrongRef 665/// 666/// Reconstructs the full CID from the stored digest 667pub fn storage_to_strongref(storage: &StrongRefStorage) -> Option<jacquard_api::com_atproto::repo::strong_ref::StrongRef<'static>> { 668 // Reconstruct the full CID from digest (record CID, not blob) 669 let cid = digest_to_jacquard_cid(&storage.cid, false)?; 670 671 // Parse AtUri from string 672 let uri: jacquard_common::types::string::AtUri<'static> = storage.uri.parse().ok()?; 673 674 // Use builder to create StrongRef 675 use jacquard_api::com_atproto::repo::strong_ref::StrongRef; 676 677 Some(StrongRef::new() 678 .uri(uri) 679 .cid(cid) 680 .build()) 681} 682 683#[cfg(test)] 684mod jacquard_tests { 685 use super::*; 686 687 #[test] 688 fn test_jacquard_cid_round_trip() { 689 let digest = vec![0u8; 32]; // Example digest 690 691 // Test blob CID 692 let blob_cid = digest_to_jacquard_cid(&digest, true).unwrap(); 693 assert!(blob_cid.as_str().starts_with("bafkrei")); // Blob CIDs start with this 694 695 // Test record CID 696 let record_cid = digest_to_jacquard_cid(&digest, false).unwrap(); 697 assert!(record_cid.as_str().starts_with("bafyrei")); // Record CIDs start with this 698 } 699}