A library for ATProtocol identities.

documentation: updating validation examples

Signed-off-by: Nick Gerakines <nick.gerakines@gmail.com>

Changed files
+519 -15
crates
atproto-identity
+519 -15
crates/atproto-identity/src/validation.rs
··· 1 1 //! Input validation for AT Protocol handles and DIDs. 2 2 //! 3 - //! Provides validation functions for various identifier formats used in the AT Protocol 4 - //! ecosystem including handles, hostnames, and DID identifiers. Follows RFC standards 5 - //! for hostname validation and AT Protocol specifications for handle formats. 3 + //! This module provides comprehensive validation functions for various identifier formats 4 + //! used in the AT Protocol ecosystem. All validation follows established standards including 5 + //! RFC 1035 for hostnames and AT Protocol specifications for handle and DID formats. 6 + //! 7 + //! # Main Functions 8 + //! 9 + //! ## Handle Validation 10 + //! - [`is_valid_handle`] - Validates and normalizes AT Protocol handles 11 + //! - [`strip_handle_prefixes`] - Removes common handle prefixes (`@`, `at://`) 12 + //! 13 + //! ## DID Validation 14 + //! - [`is_valid_did_method_plc`] - Validates PLC DIDs (`did:plc:...`) 15 + //! - [`is_valid_did_method_web`] - Validates Web DIDs (`did:web:...`) 16 + //! - [`is_valid_did_method_webvh`] - Validates WebVH DIDs (`did:webvh:...`) 17 + //! 18 + //! ## Network Address Validation 19 + //! - [`is_valid_hostname`] - RFC 1035 compliant hostname validation 20 + //! - [`is_ipv4`] - IPv4 address validation 21 + //! - [`is_ipv6`] - IPv6 address validation 22 + //! 23 + //! ## Utility Functions 24 + //! - [`is_valid_base58_btc`] - Base58-btc alphabet character validation 25 + //! 26 + //! # Examples 27 + //! 28 + //! ``` 29 + //! use atproto_identity::validation::*; 30 + //! 31 + //! // Handle validation 32 + //! assert_eq!(is_valid_handle("@alice.bsky.social"), Some("alice.bsky.social".to_string())); 33 + //! 34 + //! // DID validation 35 + //! assert!(is_valid_did_method_plc("did:plc:z3f2222fa222f5c33c2f27ez")); 36 + //! assert!(is_valid_did_method_web("did:web:example.com", true)); 37 + //! assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com", true)); 38 + //! 39 + //! // Network validation 40 + //! assert!(is_valid_hostname("example.com")); 41 + //! assert!(is_ipv4("192.168.1.1")); 42 + //! assert!(is_ipv6("2001:db8::1")); 43 + //! ``` 6 44 7 45 /// Maximum length for a valid hostname as defined in RFC 1035 8 46 const MAX_HOSTNAME_LENGTH: usize = 253; ··· 14 52 const RESERVED_TLDS: [&str; 4] = [".localhost", ".internal", ".arpa", ".local"]; 15 53 16 54 /// Validates if a string is a valid hostname according to RFC 1035. 17 - /// Checks length limits, reserved TLDs, and character validity. 18 - /// Rejects IPv4 and IPv6 addresses. 55 + /// 56 + /// A valid hostname must: 57 + /// - Be between 1 and 253 characters in length 58 + /// - Not use reserved top-level domains (.localhost, .internal, .arpa, .local) 59 + /// - Not be an IPv4 or IPv6 address 60 + /// - Contain only valid hostname characters (letters, digits, hyphens, dots) 61 + /// - Have valid DNS labels (no leading/trailing hyphens, max 63 chars per label) 62 + /// 63 + /// # Arguments 64 + /// 65 + /// * `hostname` - The hostname string to validate 66 + /// 67 + /// # Returns 68 + /// 69 + /// `true` if the hostname is valid according to RFC 1035, `false` otherwise 70 + /// 71 + /// # Examples 72 + /// 73 + /// ``` 74 + /// use atproto_identity::validation::is_valid_hostname; 75 + /// 76 + /// // Valid hostnames 77 + /// assert!(is_valid_hostname("example.com")); 78 + /// assert!(is_valid_hostname("sub.example.com")); 79 + /// assert!(is_valid_hostname("test-host.example.com")); 80 + /// assert!(is_valid_hostname("localhost")); 81 + /// 82 + /// // Invalid hostnames 83 + /// assert!(!is_valid_hostname("192.168.1.1")); // IPv4 address 84 + /// assert!(!is_valid_hostname("example.localhost")); // Reserved TLD 85 + /// assert!(!is_valid_hostname("example..com")); // Double dot 86 + /// assert!(!is_valid_hostname("-example.com")); // Leading hyphen 87 + /// ``` 19 88 pub fn is_valid_hostname(hostname: &str) -> bool { 20 89 // Empty hostnames are invalid 21 90 if hostname.is_empty() || hostname.len() > MAX_HOSTNAME_LENGTH { ··· 65 134 || label.ends_with('-')) 66 135 } 67 136 68 - /// Checks if a string is a valid IPv4 address 137 + /// Checks if a string is a valid IPv4 address. 138 + /// 139 + /// Validates that the string consists of exactly four decimal numbers 140 + /// separated by dots, where each number is between 0 and 255. 141 + /// 142 + /// # Arguments 143 + /// 144 + /// * `s` - The string to validate as an IPv4 address 145 + /// 146 + /// # Returns 147 + /// 148 + /// `true` if the string is a valid IPv4 address, `false` otherwise 149 + /// 150 + /// # Examples 151 + /// 152 + /// ``` 153 + /// use atproto_identity::validation::is_ipv4; 154 + /// 155 + /// // Valid IPv4 addresses 156 + /// assert!(is_ipv4("192.168.1.1")); 157 + /// assert!(is_ipv4("127.0.0.1")); 158 + /// assert!(is_ipv4("255.255.255.255")); 159 + /// assert!(is_ipv4("0.0.0.0")); 160 + /// 161 + /// // Invalid IPv4 addresses 162 + /// assert!(!is_ipv4("256.1.1.1")); // Number too large 163 + /// assert!(!is_ipv4("192.168.1")); // Missing octet 164 + /// assert!(!is_ipv4("192.168.1.1.1")); // Too many octets 165 + /// assert!(!is_ipv4("example.com")); // Not numeric 166 + /// ``` 69 167 pub fn is_ipv4(s: &str) -> bool { 70 168 let parts: Vec<&str> = s.split('.').collect(); 71 169 if parts.len() != 4 { ··· 75 173 parts.iter().all(|part| part.parse::<u8>().is_ok()) 76 174 } 77 175 78 - /// Checks if a string is a valid IPv6 address 176 + /// Checks if a string is a valid IPv6 address. 177 + /// 178 + /// Performs basic IPv6 validation including: 179 + /// - Must contain colons (distinguishing from IPv4) 180 + /// - Supports brackets for URLs (e.g., `[2001:db8::1]`) 181 + /// - Validates compressed notation with `::` (at most one occurrence) 182 + /// - Each segment must be valid hexadecimal (1-4 characters) 183 + /// - At most 8 segments total 184 + /// 185 + /// # Arguments 186 + /// 187 + /// * `s` - The string to validate as an IPv6 address 188 + /// 189 + /// # Returns 190 + /// 191 + /// `true` if the string is a valid IPv6 address, `false` otherwise 192 + /// 193 + /// # Examples 194 + /// 195 + /// ``` 196 + /// use atproto_identity::validation::is_ipv6; 197 + /// 198 + /// // Valid IPv6 addresses 199 + /// assert!(is_ipv6("2001:db8::1")); 200 + /// assert!(is_ipv6("::1")); 201 + /// assert!(is_ipv6("fe80::1")); 202 + /// assert!(is_ipv6("[2001:db8::1]")); // With brackets 203 + /// assert!(is_ipv6("2001:0db8:0000:0000:0000:ff00:0042:8329")); 204 + /// 205 + /// // Invalid IPv6 addresses 206 + /// assert!(!is_ipv6("192.168.1.1")); // IPv4, not IPv6 207 + /// assert!(!is_ipv6("example.com")); // No colons 208 + /// assert!(!is_ipv6("2001:gggg::1")); // Invalid hex characters 209 + /// ``` 79 210 pub fn is_ipv6(s: &str) -> bool { 80 211 // Basic IPv6 validation - must contain colons and valid hex characters 81 212 if !s.contains(':') { ··· 110 241 } 111 242 112 243 /// Validates and normalizes an AT Protocol handle. 113 - /// Returns the normalized handle if valid, None otherwise. 244 + /// 245 + /// A valid AT Protocol handle must: 246 + /// - Be a valid hostname (after stripping prefixes) 247 + /// - Contain at least one period (to distinguish from simple hostnames) 248 + /// - Follow all hostname validation rules (RFC 1035) 249 + /// 250 + /// The function automatically strips common prefixes (`at://` and `@`) before validation. 251 + /// 252 + /// # Arguments 253 + /// 254 + /// * `handle` - The handle string to validate and normalize 255 + /// 256 + /// # Returns 257 + /// 258 + /// `Some(String)` containing the normalized handle if valid, `None` if invalid 259 + /// 260 + /// # Examples 261 + /// 262 + /// ``` 263 + /// use atproto_identity::validation::is_valid_handle; 264 + /// 265 + /// // Valid handles 266 + /// assert_eq!(is_valid_handle("alice.bsky.social"), Some("alice.bsky.social".to_string())); 267 + /// assert_eq!(is_valid_handle("@bob.example.com"), Some("bob.example.com".to_string())); 268 + /// assert_eq!(is_valid_handle("at://charlie.test.com"), Some("charlie.test.com".to_string())); 269 + /// 270 + /// // Invalid handles 271 + /// assert_eq!(is_valid_handle("localhost"), None); // No period 272 + /// assert_eq!(is_valid_handle("192.168.1.1"), None); // IPv4 address 273 + /// assert_eq!(is_valid_handle("invalid..handle.com"), None); // Double dot 274 + /// ``` 114 275 pub fn is_valid_handle(handle: &str) -> Option<String> { 115 276 // Strip optional prefixes to get the core handle 116 277 let trimmed = strip_handle_prefixes(handle); ··· 123 284 } 124 285 } 125 286 287 + /// Strips common AT Protocol handle prefixes from a handle string. 288 + /// 289 + /// Removes the `at://` or `@` prefix if present, returning the clean handle. 290 + /// This is useful for normalizing handle input from various sources. 291 + /// 292 + /// # Arguments 293 + /// 294 + /// * `handle` - The handle string that may contain prefixes 295 + /// 296 + /// # Returns 297 + /// 298 + /// The handle string with prefixes removed 299 + /// 300 + /// # Examples 301 + /// 302 + /// ``` 303 + /// use atproto_identity::validation::strip_handle_prefixes; 304 + /// 305 + /// assert_eq!(strip_handle_prefixes("@alice.bsky.social"), "alice.bsky.social"); 306 + /// assert_eq!(strip_handle_prefixes("at://bob.example.com"), "bob.example.com"); 307 + /// assert_eq!(strip_handle_prefixes("charlie.test.com"), "charlie.test.com"); 308 + /// ``` 126 309 pub fn strip_handle_prefixes(handle: &str) -> &str { 127 310 if let Some(value) = handle.strip_prefix("at://") { 128 311 value ··· 134 317 } 135 318 136 319 /// Validates if a string is a properly formatted PLC DID. 137 - /// Checks for correct prefix and 24-character base32 identifier. 320 + /// 321 + /// A valid PLC DID must: 322 + /// - Start with the prefix `did:plc:` 323 + /// - Be followed by exactly 24 characters of base32 encoding (lowercase letters a-z and digits 2-7) 324 + /// 325 + /// # Arguments 326 + /// 327 + /// * `did` - The DID string to validate 328 + /// 329 + /// # Returns 330 + /// 331 + /// `true` if the DID is a valid PLC DID, `false` otherwise 332 + /// 333 + /// # Examples 334 + /// 335 + /// ``` 336 + /// use atproto_identity::validation::is_valid_did_method_plc; 337 + /// 338 + /// // Valid PLC DIDs 339 + /// assert!(is_valid_did_method_plc("did:plc:z3f2222fa222f5c33c2f27ez")); 340 + /// assert!(is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuvwx")); 341 + /// 342 + /// // Invalid PLC DIDs 343 + /// assert!(!is_valid_did_method_plc("did:web:example.com")); 344 + /// assert!(!is_valid_did_method_plc("did:plc:invalid0length")); 345 + /// assert!(!is_valid_did_method_plc("did:plc:UPPERCASE_NOT_ALLOWED")); 346 + /// ``` 138 347 pub fn is_valid_did_method_plc(did: &str) -> bool { 139 348 let did_value = match did.strip_prefix("did:plc:") { 140 349 Some(value) => value, ··· 149 358 } 150 359 151 360 /// Validates if a string is a properly formatted Web DID. 152 - /// Checks for correct prefix and validates the content after the prefix. 153 - /// 154 - /// When `strict` is true, only a valid hostname is allowed after "did:web:". 155 - /// When `strict` is false, the value can contain colon-separated segments where: 156 - /// - The first segment must be a valid hostname 157 - /// - Any subsequent segments must be non-empty alphanumeric strings 361 + /// 362 + /// A valid Web DID must start with the prefix `did:web:` followed by content that 363 + /// depends on the strictness mode: 364 + /// 365 + /// # Strict Mode (`strict = true`) 366 + /// - Only a valid hostname is allowed after `did:web:` 367 + /// - No additional path segments permitted 368 + /// 369 + /// # Non-Strict Mode (`strict = false`) 370 + /// - First segment must be a valid hostname 371 + /// - Additional colon-separated segments are allowed 372 + /// - Each additional segment must be non-empty and alphanumeric 373 + /// 374 + /// # Arguments 375 + /// 376 + /// * `did` - The DID string to validate 377 + /// * `strict` - Whether to use strict hostname-only validation 378 + /// 379 + /// # Returns 380 + /// 381 + /// `true` if the DID is a valid Web DID according to the specified mode, `false` otherwise 382 + /// 383 + /// # Examples 384 + /// 385 + /// ``` 386 + /// use atproto_identity::validation::is_valid_did_method_web; 387 + /// 388 + /// // Valid in both modes 389 + /// assert!(is_valid_did_method_web("did:web:example.com", true)); 390 + /// assert!(is_valid_did_method_web("did:web:example.com", false)); 391 + /// 392 + /// // Valid only in non-strict mode 393 + /// assert!(!is_valid_did_method_web("did:web:example.com:path", true)); 394 + /// assert!(is_valid_did_method_web("did:web:example.com:path", false)); 395 + /// assert!(is_valid_did_method_web("did:web:example.com:path:subpath", false)); 396 + /// 397 + /// // Invalid in both modes 398 + /// assert!(!is_valid_did_method_web("did:web:192.168.1.1", true)); 399 + /// assert!(!is_valid_did_method_web("did:web:example.com:", false)); 400 + /// ``` 158 401 pub fn is_valid_did_method_web(did: &str, strict: bool) -> bool { 159 402 let did_value = match did.strip_prefix("did:web:") { 160 403 Some(value) => value, ··· 185 428 } 186 429 } 187 430 431 + /// Validates if a string is a properly formatted WebVH DID. 432 + /// 433 + /// A WebVH DID extends the Web DID format by adding a SCIM (Self-Controlled Identity Marker) 434 + /// segment immediately after the `did:webvh:` prefix. 435 + /// 436 + /// # Format 437 + /// 438 + /// ```text 439 + /// did:webvh:<scim>:<content> 440 + /// ``` 441 + /// 442 + /// Where: 443 + /// - `<scim>` must contain only base58-btc alphabet characters (`123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`) 444 + /// - `<content>` follows the same validation rules as `did:web` content 445 + /// 446 + /// # Strict vs Non-Strict Mode 447 + /// 448 + /// **Strict Mode (`strict = true`)**: 449 + /// - `<content>` must be a valid hostname only 450 + /// - No additional path segments permitted 451 + /// 452 + /// **Non-Strict Mode (`strict = false`)**: 453 + /// - First segment of `<content>` must be a valid hostname 454 + /// - Additional colon-separated segments are allowed 455 + /// - Each additional segment must be non-empty and alphanumeric 456 + /// 457 + /// # Arguments 458 + /// 459 + /// * `did` - The DID string to validate 460 + /// * `strict` - Whether to use strict hostname-only validation for the content portion 461 + /// 462 + /// # Returns 463 + /// 464 + /// `true` if the DID is a valid WebVH DID according to the specified mode, `false` otherwise 465 + /// 466 + /// # Examples 467 + /// 468 + /// ``` 469 + /// use atproto_identity::validation::is_valid_did_method_webvh; 470 + /// 471 + /// // Valid WebVH DIDs in both modes 472 + /// assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com", true)); 473 + /// assert!(is_valid_did_method_webvh("did:webvh:XYZ789:sub.example.com", false)); 474 + /// 475 + /// // Valid only in non-strict mode (has path segments) 476 + /// assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path", true)); 477 + /// assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com:path", false)); 478 + /// assert!(is_valid_did_method_webvh("did:webvh:def456:example.com:path:subpath", false)); 479 + /// 480 + /// // Invalid - SCIM contains excluded base58 characters (0, O, I, l) 481 + /// assert!(!is_valid_did_method_webvh("did:webvh:0abc:example.com", true)); 482 + /// assert!(!is_valid_did_method_webvh("did:webvh:Oabc:example.com", false)); 483 + /// assert!(!is_valid_did_method_webvh("did:webvh:Iabc:example.com", true)); 484 + /// assert!(!is_valid_did_method_webvh("did:webvh:labc:example.com", false)); 485 + /// 486 + /// // Invalid - wrong format or missing components 487 + /// assert!(!is_valid_did_method_webvh("did:web:abc123:example.com", true)); // Wrong prefix 488 + /// assert!(!is_valid_did_method_webvh("did:webvh:abc123", true)); // Missing content 489 + /// assert!(!is_valid_did_method_webvh("did:webvh::example.com", true)); // Empty SCIM 490 + /// ``` 491 + pub fn is_valid_did_method_webvh(did: &str, strict: bool) -> bool { 492 + let did_value = match did.strip_prefix("did:webvh:") { 493 + Some(value) => value, 494 + None => return false, 495 + }; 496 + 497 + // Split by the first colon to separate scim from content 498 + let parts: Vec<&str> = did_value.splitn(2, ':').collect(); 499 + 500 + // Must have exactly 2 parts: scim and content 501 + if parts.len() != 2 { 502 + return false; 503 + } 504 + 505 + let scim = parts[0]; 506 + let content = parts[1]; 507 + 508 + // Validate scim - must be non-empty and contain only base58-btc alphabet characters 509 + if scim.is_empty() || !is_valid_base58_btc(scim) { 510 + return false; 511 + } 512 + 513 + // Validate content using the same rules as did:web 514 + if strict { 515 + // In strict mode, only a valid hostname is allowed 516 + is_valid_hostname(content) 517 + } else { 518 + // In non-strict mode, allow colon-separated segments 519 + let segments: Vec<&str> = content.split(':').collect(); 520 + 521 + // Must have at least one segment (the hostname) 522 + if segments.is_empty() { 523 + return false; 524 + } 525 + 526 + // First segment must be a valid hostname 527 + if !is_valid_hostname(segments[0]) { 528 + return false; 529 + } 530 + 531 + // All subsequent segments must be non-empty alphanumeric strings 532 + segments[1..].iter().all(|segment| { 533 + !segment.is_empty() && segment.chars().all(|c| c.is_ascii_alphanumeric()) 534 + }) 535 + } 536 + } 537 + 538 + /// Checks if a string contains only base58-btc alphabet characters. 539 + /// 540 + /// The base58-btc alphabet is used in Bitcoin and other cryptocurrency systems. 541 + /// It includes all alphanumeric characters except those that are easily confused: 542 + /// - Excludes: `0` (zero), `O` (capital O), `I` (capital I), `l` (lowercase L) 543 + /// - Includes: `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz` 544 + /// 545 + /// # Arguments 546 + /// 547 + /// * `s` - The string to validate for base58-btc character compliance 548 + /// 549 + /// # Returns 550 + /// 551 + /// `true` if the string is non-empty and contains only valid base58-btc characters, `false` otherwise 552 + /// 553 + /// # Examples 554 + /// 555 + /// ``` 556 + /// use atproto_identity::validation::is_valid_base58_btc; 557 + /// 558 + /// // Valid base58-btc strings 559 + /// assert!(is_valid_base58_btc("123456789")); 560 + /// assert!(is_valid_base58_btc("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz")); 561 + /// assert!(is_valid_base58_btc("abc123XYZ")); 562 + /// 563 + /// // Invalid - contains excluded characters 564 + /// assert!(!is_valid_base58_btc("abc0def")); // Contains 0 565 + /// assert!(!is_valid_base58_btc("abcOdef")); // Contains O 566 + /// assert!(!is_valid_base58_btc("abcIdef")); // Contains I 567 + /// assert!(!is_valid_base58_btc("abcldef")); // Contains l 568 + /// 569 + /// // Invalid - empty or non-alphanumeric 570 + /// assert!(!is_valid_base58_btc("")); 571 + /// assert!(!is_valid_base58_btc("abc-def")); 572 + /// ``` 573 + pub fn is_valid_base58_btc(s: &str) -> bool { 574 + const BASE58_ALPHABET: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; 575 + !s.is_empty() && s.chars().all(|c| BASE58_ALPHABET.contains(c)) 576 + } 577 + 188 578 #[cfg(test)] 189 579 mod tests { 190 580 use super::*; ··· 400 790 // Edge cases that should be valid 401 791 assert!(is_valid_hostname("1.2.3.example.com")); // Numbers are ok in labels 402 792 assert!(is_valid_hostname("xn--example.com")); // Punycode is valid 793 + } 794 + 795 + #[test] 796 + fn test_is_valid_did_method_webvh() { 797 + // Test strict mode - valid cases 798 + assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com", true)); 799 + assert!(is_valid_did_method_webvh("did:webvh:XYZ789:sub.example.com", true)); 800 + assert!(is_valid_did_method_webvh("did:webvh:ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz123456789:example.com", true)); 801 + assert!(is_valid_did_method_webvh("did:webvh:1:example.com", true)); // single char scim 802 + assert!(is_valid_did_method_webvh("did:webvh:zzzzzz:localhost", true)); 803 + 804 + // Test strict mode - invalid cases with path segments 805 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path", true)); 806 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path:subpath", true)); 807 + 808 + // Test non-strict mode - valid cases 809 + assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com", false)); 810 + assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com:path", false)); 811 + assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com:path:subpath", false)); 812 + assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com:123", false)); 813 + assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com:ABC123", false)); 814 + 815 + // Invalid - wrong prefix 816 + assert!(!is_valid_did_method_webvh("did:web:abc123:example.com", true)); 817 + assert!(!is_valid_did_method_webvh("did:web:abc123:example.com", false)); 818 + assert!(!is_valid_did_method_webvh("did:plc:abc123:example.com", true)); 819 + assert!(!is_valid_did_method_webvh("webvh:abc123:example.com", true)); 820 + assert!(!is_valid_did_method_webvh("abc123:example.com", true)); 821 + 822 + // Invalid - missing scim or content 823 + assert!(!is_valid_did_method_webvh("did:webvh:", true)); 824 + assert!(!is_valid_did_method_webvh("did:webvh:abc123", true)); // missing content 825 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:", true)); // empty content 826 + assert!(!is_valid_did_method_webvh("did:webvh::example.com", true)); // empty scim 827 + assert!(!is_valid_did_method_webvh("did:webvh:example.com", true)); // no scim separator 828 + 829 + // Invalid - scim contains invalid base58 characters 830 + assert!(!is_valid_did_method_webvh("did:webvh:0abc:example.com", true)); // contains 0 831 + assert!(!is_valid_did_method_webvh("did:webvh:Oabc:example.com", true)); // contains O 832 + assert!(!is_valid_did_method_webvh("did:webvh:Iabc:example.com", true)); // contains I 833 + assert!(!is_valid_did_method_webvh("did:webvh:labc:example.com", true)); // contains l 834 + assert!(!is_valid_did_method_webvh("did:webvh:abc-123:example.com", true)); // contains - 835 + assert!(!is_valid_did_method_webvh("did:webvh:abc_123:example.com", true)); // contains _ 836 + assert!(!is_valid_did_method_webvh("did:webvh:abc.123:example.com", true)); // contains . 837 + assert!(!is_valid_did_method_webvh("did:webvh:abc@123:example.com", true)); // contains @ 838 + assert!(!is_valid_did_method_webvh("did:webvh:abc 123:example.com", true)); // contains space 839 + assert!(!is_valid_did_method_webvh("did:webvh:abc!123:example.com", true)); // contains ! 840 + assert!(!is_valid_did_method_webvh("did:webvh:abc#123:example.com", true)); // contains # 841 + assert!(!is_valid_did_method_webvh("did:webvh:abc$123:example.com", true)); // contains $ 842 + 843 + // Invalid - bad hostname in content 844 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:", false)); // empty hostname 845 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:..example.com", true)); 846 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:.example.com", true)); 847 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com.", true)); 848 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:-example.com", true)); 849 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.localhost", true)); // reserved TLD 850 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:192.168.1.1", true)); // IPv4 851 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:2001:db8::1", true)); // IPv6 852 + 853 + // Invalid in non-strict mode - empty path segments 854 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:", false)); 855 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com::", false)); 856 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path:", false)); 857 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com::path", false)); 858 + 859 + // Invalid in non-strict mode - non-alphanumeric in path segments 860 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path/subpath", false)); 861 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path-name", false)); 862 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path_name", false)); 863 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path.name", false)); 864 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path@name", false)); 865 + assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path name", false)); 866 + 867 + // Edge cases with base58 characters 868 + assert!(is_valid_did_method_webvh("did:webvh:111111:example.com", true)); // all 1s 869 + assert!(is_valid_did_method_webvh("did:webvh:999999:example.com", true)); // all 9s 870 + assert!(is_valid_did_method_webvh("did:webvh:AAAAAA:example.com", true)); // all As 871 + assert!(is_valid_did_method_webvh("did:webvh:zzzzzz:example.com", true)); // all zs 872 + assert!(is_valid_did_method_webvh("did:webvh:HJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz:example.com", true)); // no excluded letters 873 + } 874 + 875 + #[test] 876 + fn test_is_valid_base58_btc() { 877 + // Valid base58 strings 878 + assert!(is_valid_base58_btc("123456789")); 879 + assert!(is_valid_base58_btc("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz")); 880 + assert!(is_valid_base58_btc("1")); 881 + assert!(is_valid_base58_btc("z")); 882 + assert!(is_valid_base58_btc("ABC123xyz")); 883 + 884 + // Invalid - contains excluded characters 885 + assert!(!is_valid_base58_btc("0")); // zero 886 + assert!(!is_valid_base58_btc("O")); // capital O 887 + assert!(!is_valid_base58_btc("I")); // capital I 888 + assert!(!is_valid_base58_btc("l")); // lowercase l 889 + assert!(!is_valid_base58_btc("abc0def")); 890 + assert!(!is_valid_base58_btc("abcOdef")); 891 + assert!(!is_valid_base58_btc("abcIdef")); 892 + assert!(!is_valid_base58_btc("abcldef")); 893 + 894 + // Invalid - contains non-alphanumeric characters 895 + assert!(!is_valid_base58_btc("abc-def")); 896 + assert!(!is_valid_base58_btc("abc_def")); 897 + assert!(!is_valid_base58_btc("abc.def")); 898 + assert!(!is_valid_base58_btc("abc@def")); 899 + assert!(!is_valid_base58_btc("abc def")); 900 + assert!(!is_valid_base58_btc("abc!def")); 901 + assert!(!is_valid_base58_btc("")); 902 + 903 + // Edge cases 904 + assert!(is_valid_base58_btc("i")); // lowercase i is allowed 905 + assert!(is_valid_base58_btc("o")); // lowercase o is allowed 906 + assert!(is_valid_base58_btc("ioio")); // lowercase i and o are allowed 403 907 } 404 908 }