atproto_identity/
validation.rs

1//! Input validation for AT Protocol handles and DIDs.
2//!
3//! Validates AT Protocol identifiers including handles, DIDs, and TLDs
4//! following RFC 1035 and AT Protocol specifications.
5//! - [`strip_handle_prefixes`] - Removes common handle prefixes (`@`, `at://`)
6//!
7//! ## DID Validation  
8//! - [`is_valid_did_method_plc`] - Validates PLC DIDs (`did:plc:...`)
9//! - [`is_valid_did_method_web`] - Validates Web DIDs (`did:web:...`)
10//! - [`is_valid_did_method_webvh`] - Validates WebVH DIDs (`did:webvh:...`)
11//!
12//! ## Network Address Validation
13//! - [`is_valid_hostname`] - RFC 1035 compliant hostname validation
14//! - [`is_ipv4`] - IPv4 address validation
15//! - [`is_ipv6`] - IPv6 address validation
16//!
17//! ## Utility Functions
18//! - [`is_valid_base58_btc`] - Base58-btc alphabet character validation
19//!
20//! # Examples
21//!
22//! ```
23//! use atproto_identity::validation::*;
24//!
25//! // Handle validation
26//! assert_eq!(is_valid_handle("@alice.bsky.social"), Some("alice.bsky.social".to_string()));
27//!
28//! // DID validation
29//! assert!(is_valid_did_method_plc("did:plc:z3f2222fa222f5c33c2f27ez"));
30//! assert!(is_valid_did_method_web("did:web:example.com", true));
31//! assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com", true));
32//!
33//! // Network validation
34//! assert!(is_valid_hostname("example.com"));
35//! assert!(is_ipv4("192.168.1.1"));
36//! assert!(is_ipv6("2001:db8::1"));
37//! ```
38
39/// Maximum length for a valid hostname as defined in RFC 1035
40const MAX_HOSTNAME_LENGTH: usize = 253;
41
42/// Maximum length for a DNS label (component between dots) as defined in RFC 1035
43const MAX_LABEL_LENGTH: usize = 63;
44
45/// List of reserved top-level domains that are not valid for AT Protocol handles
46const RESERVED_TLDS: [&str; 4] = [".localhost", ".internal", ".arpa", ".local"];
47
48/// Validates if a string is a valid hostname according to RFC 1035.
49///
50/// A valid hostname must:
51/// - Be between 1 and 253 characters in length
52/// - Not use reserved top-level domains (.localhost, .internal, .arpa, .local)
53/// - Not be an IPv4 or IPv6 address
54/// - Contain only valid hostname characters (letters, digits, hyphens, dots)
55/// - Have valid DNS labels (no leading/trailing hyphens, max 63 chars per label)
56///
57/// # Arguments
58///
59/// * `hostname` - The hostname string to validate
60///
61/// # Returns
62///
63/// `true` if the hostname is valid according to RFC 1035, `false` otherwise
64///
65/// # Examples
66///
67/// ```
68/// use atproto_identity::validation::is_valid_hostname;
69///
70/// // Valid hostnames
71/// assert!(is_valid_hostname("example.com"));
72/// assert!(is_valid_hostname("sub.example.com"));
73/// assert!(is_valid_hostname("test-host.example.com"));
74/// assert!(is_valid_hostname("localhost"));
75///
76/// // Invalid hostnames
77/// assert!(!is_valid_hostname("192.168.1.1")); // IPv4 address
78/// assert!(!is_valid_hostname("example.localhost")); // Reserved TLD
79/// assert!(!is_valid_hostname("example..com")); // Double dot
80/// assert!(!is_valid_hostname("-example.com")); // Leading hyphen
81/// ```
82pub fn is_valid_hostname(hostname: &str) -> bool {
83    // Empty hostnames are invalid
84    if hostname.is_empty() || hostname.len() > MAX_HOSTNAME_LENGTH {
85        return false;
86    }
87
88    // Check if hostname uses any reserved TLDs
89    if RESERVED_TLDS.iter().any(|tld| hostname.ends_with(tld)) {
90        return false;
91    }
92
93    // Reject IPv4 addresses
94    if is_ipv4(hostname) {
95        return false;
96    }
97
98    // Reject IPv6 addresses
99    if is_ipv6(hostname) {
100        return false;
101    }
102
103    // Ensure all characters are valid hostname characters
104    if hostname.bytes().any(|byte| !is_valid_hostname_char(byte)) {
105        return false;
106    }
107
108    // Validate each DNS label in the hostname
109    if hostname.split('.').any(|label| !is_valid_dns_label(label)) {
110        return false;
111    }
112
113    true
114}
115
116fn is_valid_hostname_char(byte: u8) -> bool {
117    byte.is_ascii_lowercase()
118        || byte.is_ascii_uppercase()
119        || byte.is_ascii_digit()
120        || byte == b'-'
121        || byte == b'.'
122}
123
124fn is_valid_dns_label(label: &str) -> bool {
125    !(label.is_empty()
126        || label.len() > MAX_LABEL_LENGTH
127        || label.starts_with('-')
128        || label.ends_with('-'))
129}
130
131/// Checks if a string is a valid IPv4 address.
132///
133/// Validates that the string consists of exactly four decimal numbers
134/// separated by dots, where each number is between 0 and 255.
135///
136/// # Arguments
137///
138/// * `s` - The string to validate as an IPv4 address
139///
140/// # Returns
141///
142/// `true` if the string is a valid IPv4 address, `false` otherwise
143///
144/// # Examples
145///
146/// ```
147/// use atproto_identity::validation::is_ipv4;
148///
149/// // Valid IPv4 addresses
150/// assert!(is_ipv4("192.168.1.1"));
151/// assert!(is_ipv4("127.0.0.1"));
152/// assert!(is_ipv4("255.255.255.255"));
153/// assert!(is_ipv4("0.0.0.0"));
154///
155/// // Invalid IPv4 addresses
156/// assert!(!is_ipv4("256.1.1.1")); // Number too large
157/// assert!(!is_ipv4("192.168.1")); // Missing octet
158/// assert!(!is_ipv4("192.168.1.1.1")); // Too many octets
159/// assert!(!is_ipv4("example.com")); // Not numeric
160/// ```
161pub fn is_ipv4(s: &str) -> bool {
162    let parts: Vec<&str> = s.split('.').collect();
163    if parts.len() != 4 {
164        return false;
165    }
166
167    parts.iter().all(|part| part.parse::<u8>().is_ok())
168}
169
170/// Checks if a string is a valid IPv6 address.
171///
172/// Performs basic IPv6 validation including:
173/// - Must contain colons (distinguishing from IPv4)
174/// - Supports brackets for URLs (e.g., `[2001:db8::1]`)
175/// - Validates compressed notation with `::` (at most one occurrence)
176/// - Each segment must be valid hexadecimal (1-4 characters)
177/// - At most 8 segments total
178///
179/// # Arguments
180///
181/// * `s` - The string to validate as an IPv6 address
182///
183/// # Returns
184///
185/// `true` if the string is a valid IPv6 address, `false` otherwise
186///
187/// # Examples
188///
189/// ```
190/// use atproto_identity::validation::is_ipv6;
191///
192/// // Valid IPv6 addresses
193/// assert!(is_ipv6("2001:db8::1"));
194/// assert!(is_ipv6("::1"));
195/// assert!(is_ipv6("fe80::1"));
196/// assert!(is_ipv6("[2001:db8::1]")); // With brackets
197/// assert!(is_ipv6("2001:0db8:0000:0000:0000:ff00:0042:8329"));
198///
199/// // Invalid IPv6 addresses
200/// assert!(!is_ipv6("192.168.1.1")); // IPv4, not IPv6
201/// assert!(!is_ipv6("example.com")); // No colons
202/// assert!(!is_ipv6("2001:gggg::1")); // Invalid hex characters
203/// ```
204pub fn is_ipv6(s: &str) -> bool {
205    // Basic IPv6 validation - must contain colons and valid hex characters
206    if !s.contains(':') {
207        return false;
208    }
209
210    // Check for IPv6 with brackets
211    let s = if s.starts_with('[') && s.ends_with(']') {
212        &s[1..s.len() - 1]
213    } else {
214        s
215    };
216
217    // Split by :: for compressed notation
218    let parts: Vec<&str> = s.split("::").collect();
219    if parts.len() > 2 {
220        return false; // More than one :: is invalid
221    }
222
223    // Validate each segment
224    let segments: Vec<&str> = s.split(':').filter(|s| !s.is_empty()).collect();
225
226    // IPv6 can have at most 8 segments (or fewer with ::)
227    if segments.len() > 8 {
228        return false;
229    }
230
231    // Each segment must be valid hexadecimal and at most 4 characters
232    segments
233        .iter()
234        .all(|segment| segment.len() <= 4 && segment.chars().all(|c| c.is_ascii_hexdigit()))
235}
236
237/// Validates and normalizes an AT Protocol handle.
238///
239/// A valid AT Protocol handle must:
240/// - Be a valid hostname (after stripping prefixes)
241/// - Contain at least one period (to distinguish from simple hostnames)
242/// - Follow all hostname validation rules (RFC 1035)
243///
244/// The function automatically strips common prefixes (`at://` and `@`) before validation.
245///
246/// # Arguments
247///
248/// * `handle` - The handle string to validate and normalize
249///
250/// # Returns
251///
252/// `Some(String)` containing the normalized handle if valid, `None` if invalid
253///
254/// # Examples
255///
256/// ```
257/// use atproto_identity::validation::is_valid_handle;
258///
259/// // Valid handles
260/// assert_eq!(is_valid_handle("alice.bsky.social"), Some("alice.bsky.social".to_string()));
261/// assert_eq!(is_valid_handle("@bob.example.com"), Some("bob.example.com".to_string()));
262/// assert_eq!(is_valid_handle("at://charlie.test.com"), Some("charlie.test.com".to_string()));
263///
264/// // Invalid handles
265/// assert_eq!(is_valid_handle("localhost"), None); // No period
266/// assert_eq!(is_valid_handle("192.168.1.1"), None); // IPv4 address
267/// assert_eq!(is_valid_handle("invalid..handle.com"), None); // Double dot
268/// ```
269pub fn is_valid_handle(handle: &str) -> Option<String> {
270    // Strip optional prefixes to get the core handle
271    let trimmed = strip_handle_prefixes(handle);
272
273    // A valid handle must be a valid hostname with at least one period
274    if is_valid_hostname(trimmed) && trimmed.contains('.') {
275        Some(trimmed.to_string())
276    } else {
277        None
278    }
279}
280
281/// Strips common AT Protocol handle prefixes from a handle string.
282///
283/// Removes the `at://` or `@` prefix if present, returning the clean handle.
284/// This is useful for normalizing handle input from various sources.
285///
286/// # Arguments
287///
288/// * `handle` - The handle string that may contain prefixes
289///
290/// # Returns
291///
292/// The handle string with prefixes removed
293///
294/// # Examples
295///
296/// ```
297/// use atproto_identity::validation::strip_handle_prefixes;
298///
299/// assert_eq!(strip_handle_prefixes("@alice.bsky.social"), "alice.bsky.social");
300/// assert_eq!(strip_handle_prefixes("at://bob.example.com"), "bob.example.com");
301/// assert_eq!(strip_handle_prefixes("charlie.test.com"), "charlie.test.com");
302/// ```
303pub fn strip_handle_prefixes(handle: &str) -> &str {
304    if let Some(value) = handle.strip_prefix("at://") {
305        value
306    } else if let Some(value) = handle.strip_prefix('@') {
307        value
308    } else {
309        handle
310    }
311}
312
313/// Validates if a string is a properly formatted PLC DID.
314///
315/// A valid PLC DID must:
316/// - Start with the prefix `did:plc:`
317/// - Be followed by exactly 24 characters of base32 encoding (lowercase letters a-z and digits 2-7)
318///
319/// # Arguments
320///
321/// * `did` - The DID string to validate
322///
323/// # Returns
324///
325/// `true` if the DID is a valid PLC DID, `false` otherwise
326///
327/// # Examples
328///
329/// ```
330/// use atproto_identity::validation::is_valid_did_method_plc;
331///
332/// // Valid PLC DIDs
333/// assert!(is_valid_did_method_plc("did:plc:z3f2222fa222f5c33c2f27ez"));
334/// assert!(is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuvwx"));
335///
336/// // Invalid PLC DIDs
337/// assert!(!is_valid_did_method_plc("did:web:example.com"));
338/// assert!(!is_valid_did_method_plc("did:plc:invalid0length"));
339/// assert!(!is_valid_did_method_plc("did:plc:UPPERCASE_NOT_ALLOWED"));
340/// ```
341pub fn is_valid_did_method_plc(did: &str) -> bool {
342    let did_value = match did.strip_prefix("did:plc:") {
343        Some(value) => value,
344        None => return false,
345    };
346
347    // Must be exactly 24 characters and all valid base32 (lowercase letters and numbers 2-7)
348    did_value.len() == 24
349        && did_value
350            .chars()
351            .all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c))
352}
353
354/// Validates if a string is a properly formatted Web DID.
355///
356/// A valid Web DID must start with the prefix `did:web:` followed by content that
357/// depends on the strictness mode:
358///
359/// # Strict Mode (`strict = true`)
360/// - Only a valid hostname is allowed after `did:web:`
361/// - No additional path segments permitted
362///
363/// # Non-Strict Mode (`strict = false`)  
364/// - First segment must be a valid hostname
365/// - Additional colon-separated segments are allowed
366/// - Each additional segment must be non-empty and alphanumeric
367///
368/// # Arguments
369///
370/// * `did` - The DID string to validate
371/// * `strict` - Whether to use strict hostname-only validation
372///
373/// # Returns
374///
375/// `true` if the DID is a valid Web DID according to the specified mode, `false` otherwise
376///
377/// # Examples
378///
379/// ```
380/// use atproto_identity::validation::is_valid_did_method_web;
381///
382/// // Valid in both modes
383/// assert!(is_valid_did_method_web("did:web:example.com", true));
384/// assert!(is_valid_did_method_web("did:web:example.com", false));
385///
386/// // Valid only in non-strict mode
387/// assert!(!is_valid_did_method_web("did:web:example.com:path", true));
388/// assert!(is_valid_did_method_web("did:web:example.com:path", false));
389/// assert!(is_valid_did_method_web("did:web:example.com:path:subpath", false));
390///
391/// // Invalid in both modes
392/// assert!(!is_valid_did_method_web("did:web:192.168.1.1", true));
393/// assert!(!is_valid_did_method_web("did:web:example.com:", false));
394/// ```
395pub fn is_valid_did_method_web(did: &str, strict: bool) -> bool {
396    let did_value = match did.strip_prefix("did:web:") {
397        Some(value) => value,
398        None => return false,
399    };
400
401    if strict {
402        // In strict mode, only a valid hostname is allowed
403        is_valid_hostname(did_value)
404    } else {
405        // In non-strict mode, allow colon-separated segments
406        let segments: Vec<&str> = did_value.split(':').collect();
407
408        // Must have at least one segment (the hostname)
409        if segments.is_empty() {
410            return false;
411        }
412
413        // First segment must be a valid hostname
414        if !is_valid_hostname(segments[0]) {
415            return false;
416        }
417
418        // All subsequent segments must be non-empty alphanumeric strings
419        segments[1..].iter().all(|segment| {
420            !segment.is_empty() && segment.chars().all(|c| c.is_ascii_alphanumeric())
421        })
422    }
423}
424
425/// Validates if a string is a properly formatted WebVH DID.
426///
427/// A WebVH DID extends the Web DID format by adding a SCIM (Self-Controlled Identity Marker)
428/// segment immediately after the `did:webvh:` prefix.
429///
430/// # Format
431///
432/// ```text
433/// did:webvh:<scim>:<content>
434/// ```
435///
436/// Where:
437/// - `<scim>` must contain only base58-btc alphabet characters (`123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`)
438/// - `<content>` follows the same validation rules as `did:web` content
439///
440/// # Strict vs Non-Strict Mode
441///
442/// **Strict Mode (`strict = true`)**:
443/// - `<content>` must be a valid hostname only
444/// - No additional path segments permitted
445///
446/// **Non-Strict Mode (`strict = false`)**:
447/// - First segment of `<content>` must be a valid hostname
448/// - Additional colon-separated segments are allowed
449/// - Each additional segment must be non-empty and alphanumeric
450///
451/// # Arguments
452///
453/// * `did` - The DID string to validate
454/// * `strict` - Whether to use strict hostname-only validation for the content portion
455///
456/// # Returns
457///
458/// `true` if the DID is a valid WebVH DID according to the specified mode, `false` otherwise
459///
460/// # Examples
461///
462/// ```
463/// use atproto_identity::validation::is_valid_did_method_webvh;
464///
465/// // Valid WebVH DIDs in both modes
466/// assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com", true));
467/// assert!(is_valid_did_method_webvh("did:webvh:XYZ789:sub.example.com", false));
468///
469/// // Valid only in non-strict mode (has path segments)
470/// assert!(!is_valid_did_method_webvh("did:webvh:abc123:example.com:path", true));
471/// assert!(is_valid_did_method_webvh("did:webvh:abc123:example.com:path", false));
472/// assert!(is_valid_did_method_webvh("did:webvh:def456:example.com:path:subpath", false));
473///
474/// // Invalid - SCIM contains excluded base58 characters (0, O, I, l)
475/// assert!(!is_valid_did_method_webvh("did:webvh:0abc:example.com", true));
476/// assert!(!is_valid_did_method_webvh("did:webvh:Oabc:example.com", false));
477/// assert!(!is_valid_did_method_webvh("did:webvh:Iabc:example.com", true));
478/// assert!(!is_valid_did_method_webvh("did:webvh:labc:example.com", false));
479///
480/// // Invalid - wrong format or missing components
481/// assert!(!is_valid_did_method_webvh("did:web:abc123:example.com", true)); // Wrong prefix
482/// assert!(!is_valid_did_method_webvh("did:webvh:abc123", true)); // Missing content
483/// assert!(!is_valid_did_method_webvh("did:webvh::example.com", true)); // Empty SCIM
484/// ```
485pub fn is_valid_did_method_webvh(did: &str, strict: bool) -> bool {
486    let did_value = match did.strip_prefix("did:webvh:") {
487        Some(value) => value,
488        None => return false,
489    };
490
491    // Split by the first colon to separate scim from content
492    let parts: Vec<&str> = did_value.splitn(2, ':').collect();
493
494    // Must have exactly 2 parts: scim and content
495    if parts.len() != 2 {
496        return false;
497    }
498
499    let scim = parts[0];
500    let content = parts[1];
501
502    // Validate scim - must be non-empty and contain only base58-btc alphabet characters
503    if scim.is_empty() || !is_valid_base58_btc(scim) {
504        return false;
505    }
506
507    // Validate content using the same rules as did:web
508    if strict {
509        // In strict mode, only a valid hostname is allowed
510        is_valid_hostname(content)
511    } else {
512        // In non-strict mode, allow colon-separated segments
513        let segments: Vec<&str> = content.split(':').collect();
514
515        // Must have at least one segment (the hostname)
516        if segments.is_empty() {
517            return false;
518        }
519
520        // First segment must be a valid hostname
521        if !is_valid_hostname(segments[0]) {
522            return false;
523        }
524
525        // All subsequent segments must be non-empty alphanumeric strings
526        segments[1..].iter().all(|segment| {
527            !segment.is_empty() && segment.chars().all(|c| c.is_ascii_alphanumeric())
528        })
529    }
530}
531
532/// Checks if a string contains only base58-btc alphabet characters.
533///
534/// The base58-btc alphabet is used in Bitcoin and other cryptocurrency systems.
535/// It includes all alphanumeric characters except those that are easily confused:
536/// - Excludes: `0` (zero), `O` (capital O), `I` (capital I), `l` (lowercase L)
537/// - Includes: `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`
538///
539/// # Arguments
540///
541/// * `s` - The string to validate for base58-btc character compliance
542///
543/// # Returns
544///
545/// `true` if the string is non-empty and contains only valid base58-btc characters, `false` otherwise
546///
547/// # Examples
548///
549/// ```
550/// use atproto_identity::validation::is_valid_base58_btc;
551///
552/// // Valid base58-btc strings
553/// assert!(is_valid_base58_btc("123456789"));
554/// assert!(is_valid_base58_btc("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"));
555/// assert!(is_valid_base58_btc("abc123XYZ"));
556///
557/// // Invalid - contains excluded characters
558/// assert!(!is_valid_base58_btc("abc0def")); // Contains 0
559/// assert!(!is_valid_base58_btc("abcOdef")); // Contains O
560/// assert!(!is_valid_base58_btc("abcIdef")); // Contains I  
561/// assert!(!is_valid_base58_btc("abcldef")); // Contains l
562///
563/// // Invalid - empty or non-alphanumeric
564/// assert!(!is_valid_base58_btc(""));
565/// assert!(!is_valid_base58_btc("abc-def"));
566/// ```
567pub fn is_valid_base58_btc(s: &str) -> bool {
568    const BASE58_ALPHABET: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
569    !s.is_empty() && s.chars().all(|c| BASE58_ALPHABET.contains(c))
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn test_is_valid_did_method_plc() {
578        // Valid PLC DIDs - exactly 24 base32 characters after "did:plc:"
579        assert!(is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuvwx"));
580        assert!(is_valid_did_method_plc("did:plc:z3f2222fa222f5c33c2f27ez"));
581        assert!(is_valid_did_method_plc("did:plc:aaaaaaaaaaaaaaaaaaaaaaaa")); // 24 'a's
582        assert!(is_valid_did_method_plc("did:plc:abcdef2345ghijk6mn7pqrst")); // mix of letters and valid numbers
583
584        // Invalid PLC DIDs - contains uppercase letters (not valid base32)
585        assert!(!is_valid_did_method_plc("did:plc:ABCDEFGHIJKLMNOPQRSTUVWX"));
586        assert!(!is_valid_did_method_plc("did:plc:Abcdefghijklmnopqrstuvwx"));
587
588        // Invalid PLC DIDs - contains invalid numbers (0, 1, 8, 9)
589        assert!(!is_valid_did_method_plc("did:plc:123456789012345678901234"));
590        assert!(!is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuv0x"));
591        assert!(!is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuv1x"));
592        assert!(!is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuv8x"));
593        assert!(!is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuv9x"));
594
595        // Invalid PLC DIDs - wrong prefix
596        assert!(!is_valid_did_method_plc("did:web:abcdefghijklmnopqrstuvwx"));
597        assert!(!is_valid_did_method_plc("did:key:abcdefghijklmnopqrstuvwx"));
598        assert!(!is_valid_did_method_plc("plc:abcdefghijklmnopqrstuvwx"));
599        assert!(!is_valid_did_method_plc("abcdefghijklmnopqrstuvwx"));
600
601        // Invalid PLC DIDs - wrong length (not 24 characters)
602        assert!(!is_valid_did_method_plc("did:plc:"));
603        assert!(!is_valid_did_method_plc("did:plc:abc"));
604        assert!(!is_valid_did_method_plc("did:plc:abcdefghijklmnopqrstuv")); // 23 chars
605        assert!(!is_valid_did_method_plc(
606            "did:plc:abcdefghijklmnopqrstuvwxy"
607        )); // 25 chars
608        assert!(!is_valid_did_method_plc(
609            "did:plc:abcdefghijklmnopqrstuvwxyz"
610        )); // 26 chars
611
612        // Edge cases
613        assert!(!is_valid_did_method_plc(""));
614        assert!(!is_valid_did_method_plc("did:plc"));
615        assert!(!is_valid_did_method_plc("did:plc:"));
616        assert!(!is_valid_did_method_plc("DID:PLC:abcdefghijklmnopqrstuvwx")); // uppercase prefix
617        assert!(!is_valid_did_method_plc("did:PLC:abcdefghijklmnopqrstuvwx")); // uppercase method
618        assert!(!is_valid_did_method_plc(
619            " did:plc:abcdefghijklmnopqrstuvwx"
620        )); // leading space
621        assert!(!is_valid_did_method_plc(
622            "did:plc:abcdefghijklmnopqrstuvwx "
623        )); // trailing space
624
625        // Invalid - special characters (not base32)
626        assert!(!is_valid_did_method_plc("did:plc:abc-def_hij.klm~nop!qrst")); // special chars
627        assert!(!is_valid_did_method_plc("did:plc:~~~!!!@@@###$$$%%%^^^&")); // special chars
628        assert!(!is_valid_did_method_plc("did:plc:                        ")); // spaces
629    }
630
631    #[test]
632    fn test_is_valid_did_method_web() {
633        // Test strict mode (only hostname allowed)
634        assert!(is_valid_did_method_web("did:web:example.com", true));
635        assert!(is_valid_did_method_web("did:web:sub.example.com", true));
636        assert!(is_valid_did_method_web("did:web:example.co.uk", true));
637        assert!(is_valid_did_method_web("did:web:localhost", true));
638
639        // Invalid in strict mode - contains colon-separated segments
640        assert!(!is_valid_did_method_web("did:web:example.com:path", true));
641        assert!(!is_valid_did_method_web(
642            "did:web:example.com:path:subpath",
643            true
644        ));
645        assert!(!is_valid_did_method_web("did:web:example.com:123", true));
646
647        // Test non-strict mode (allows colon-separated segments)
648        assert!(is_valid_did_method_web("did:web:example.com", false));
649        assert!(is_valid_did_method_web("did:web:example.com:path", false));
650        assert!(is_valid_did_method_web(
651            "did:web:example.com:path:subpath",
652            false
653        ));
654        assert!(is_valid_did_method_web("did:web:example.com:123", false));
655        assert!(is_valid_did_method_web("did:web:example.com:abc123", false));
656        assert!(is_valid_did_method_web(
657            "did:web:example.com:UPPERCASE",
658            false
659        ));
660
661        // Invalid in non-strict mode - empty segments
662        assert!(!is_valid_did_method_web("did:web:example.com:", false));
663        assert!(!is_valid_did_method_web("did:web:example.com::", false));
664        assert!(!is_valid_did_method_web("did:web:example.com:path:", false));
665        assert!(!is_valid_did_method_web("did:web:example.com::path", false));
666
667        // Invalid in non-strict mode - non-alphanumeric in segments
668        assert!(!is_valid_did_method_web(
669            "did:web:example.com:path/subpath",
670            false
671        ));
672        assert!(!is_valid_did_method_web(
673            "did:web:example.com:path-name",
674            false
675        ));
676        assert!(!is_valid_did_method_web(
677            "did:web:example.com:path_name",
678            false
679        ));
680        assert!(!is_valid_did_method_web(
681            "did:web:example.com:path.name",
682            false
683        ));
684        assert!(!is_valid_did_method_web(
685            "did:web:example.com:path@name",
686            false
687        ));
688        assert!(!is_valid_did_method_web(
689            "did:web:example.com:path name",
690            false
691        ));
692
693        // Invalid in both modes - wrong prefix
694        assert!(!is_valid_did_method_web("did:plc:example.com", true));
695        assert!(!is_valid_did_method_web("did:plc:example.com", false));
696        assert!(!is_valid_did_method_web("web:example.com", true));
697        assert!(!is_valid_did_method_web("web:example.com", false));
698        assert!(!is_valid_did_method_web("example.com", true));
699        assert!(!is_valid_did_method_web("example.com", false));
700
701        // Invalid in both modes - invalid hostname
702        assert!(!is_valid_did_method_web("did:web:", true));
703        assert!(!is_valid_did_method_web("did:web:", false));
704        assert!(!is_valid_did_method_web("did:web:example..com", true));
705        assert!(!is_valid_did_method_web("did:web:example..com", false));
706        assert!(!is_valid_did_method_web("did:web:.example.com", true));
707        assert!(!is_valid_did_method_web("did:web:.example.com", false));
708        assert!(!is_valid_did_method_web("did:web:example.com.", true));
709        assert!(!is_valid_did_method_web("did:web:example.com.", false));
710        assert!(!is_valid_did_method_web("did:web:-example.com", true));
711        assert!(!is_valid_did_method_web("did:web:-example.com", false));
712
713        // Invalid in both modes - reserved TLDs
714        assert!(!is_valid_did_method_web("did:web:example.localhost", true));
715        assert!(!is_valid_did_method_web("did:web:example.localhost", false));
716        assert!(!is_valid_did_method_web("did:web:example.local", true));
717        assert!(!is_valid_did_method_web("did:web:example.local", false));
718
719        // Invalid in both modes - IPv4 addresses
720        assert!(!is_valid_did_method_web("did:web:192.168.1.1", true));
721        assert!(!is_valid_did_method_web("did:web:192.168.1.1", false));
722        assert!(!is_valid_did_method_web("did:web:127.0.0.1", true));
723        assert!(!is_valid_did_method_web("did:web:127.0.0.1", false));
724        assert!(!is_valid_did_method_web("did:web:10.0.0.1", true));
725        assert!(!is_valid_did_method_web("did:web:10.0.0.1", false));
726
727        // Invalid in both modes - IPv6 addresses
728        assert!(!is_valid_did_method_web("did:web:2001:db8::1", true));
729        assert!(!is_valid_did_method_web("did:web:2001:db8::1", false));
730        assert!(!is_valid_did_method_web("did:web:::1", true));
731        assert!(!is_valid_did_method_web("did:web:::1", false));
732        assert!(!is_valid_did_method_web("did:web:[2001:db8::1]", true));
733        assert!(!is_valid_did_method_web("did:web:[2001:db8::1]", false));
734    }
735
736    #[test]
737    fn test_is_valid_hostname() {
738        // Valid hostnames
739        assert!(is_valid_hostname("example.com"));
740        assert!(is_valid_hostname("sub.example.com"));
741        assert!(is_valid_hostname("example.co.uk"));
742        assert!(is_valid_hostname("localhost"));
743        assert!(is_valid_hostname("test-host.example.com"));
744        assert!(is_valid_hostname("123.example.com"));
745        assert!(is_valid_hostname("a.b.c.d.example.com"));
746
747        // Invalid - IPv4 addresses
748        assert!(!is_valid_hostname("192.168.1.1"));
749        assert!(!is_valid_hostname("127.0.0.1"));
750        assert!(!is_valid_hostname("10.0.0.1"));
751        assert!(!is_valid_hostname("255.255.255.255"));
752        assert!(!is_valid_hostname("0.0.0.0"));
753
754        // Invalid - IPv6 addresses
755        assert!(!is_valid_hostname("2001:db8::1"));
756        assert!(!is_valid_hostname("::1"));
757        assert!(!is_valid_hostname("fe80::1"));
758        assert!(!is_valid_hostname("[2001:db8::1]"));
759        assert!(!is_valid_hostname("[::1]"));
760        assert!(!is_valid_hostname(
761            "2001:0db8:0000:0000:0000:ff00:0042:8329"
762        ));
763
764        // Invalid - empty or too long
765        assert!(!is_valid_hostname(""));
766        assert!(!is_valid_hostname(&"a".repeat(254))); // Too long
767
768        // Invalid - reserved TLDs
769        assert!(!is_valid_hostname("example.localhost"));
770        assert!(!is_valid_hostname("example.local"));
771        assert!(!is_valid_hostname("example.internal"));
772        assert!(!is_valid_hostname("example.arpa"));
773
774        // Invalid - bad format
775        assert!(!is_valid_hostname("example..com"));
776        assert!(!is_valid_hostname(".example.com"));
777        assert!(!is_valid_hostname("example.com."));
778        assert!(!is_valid_hostname("-example.com"));
779        assert!(!is_valid_hostname("example-.com"));
780        assert!(!is_valid_hostname("exam ple.com"));
781        assert!(!is_valid_hostname("exam@ple.com"));
782        assert!(!is_valid_hostname("exam_ple.com"));
783
784        // Edge cases that should be valid
785        assert!(is_valid_hostname("1.2.3.example.com")); // Numbers are ok in labels
786        assert!(is_valid_hostname("xn--example.com")); // Punycode is valid
787    }
788
789    #[test]
790    fn test_is_valid_did_method_webvh() {
791        // Test strict mode - valid cases
792        assert!(is_valid_did_method_webvh(
793            "did:webvh:abc123:example.com",
794            true
795        ));
796        assert!(is_valid_did_method_webvh(
797            "did:webvh:XYZ789:sub.example.com",
798            true
799        ));
800        assert!(is_valid_did_method_webvh(
801            "did:webvh:ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz123456789:example.com",
802            true
803        ));
804        assert!(is_valid_did_method_webvh("did:webvh:1:example.com", true)); // single char scim
805        assert!(is_valid_did_method_webvh(
806            "did:webvh:zzzzzz:localhost",
807            true
808        ));
809
810        // Test strict mode - invalid cases with path segments
811        assert!(!is_valid_did_method_webvh(
812            "did:webvh:abc123:example.com:path",
813            true
814        ));
815        assert!(!is_valid_did_method_webvh(
816            "did:webvh:abc123:example.com:path:subpath",
817            true
818        ));
819
820        // Test non-strict mode - valid cases
821        assert!(is_valid_did_method_webvh(
822            "did:webvh:abc123:example.com",
823            false
824        ));
825        assert!(is_valid_did_method_webvh(
826            "did:webvh:abc123:example.com:path",
827            false
828        ));
829        assert!(is_valid_did_method_webvh(
830            "did:webvh:abc123:example.com:path:subpath",
831            false
832        ));
833        assert!(is_valid_did_method_webvh(
834            "did:webvh:abc123:example.com:123",
835            false
836        ));
837        assert!(is_valid_did_method_webvh(
838            "did:webvh:abc123:example.com:ABC123",
839            false
840        ));
841
842        // Invalid - wrong prefix
843        assert!(!is_valid_did_method_webvh(
844            "did:web:abc123:example.com",
845            true
846        ));
847        assert!(!is_valid_did_method_webvh(
848            "did:web:abc123:example.com",
849            false
850        ));
851        assert!(!is_valid_did_method_webvh(
852            "did:plc:abc123:example.com",
853            true
854        ));
855        assert!(!is_valid_did_method_webvh("webvh:abc123:example.com", true));
856        assert!(!is_valid_did_method_webvh("abc123:example.com", true));
857
858        // Invalid - missing scim or content
859        assert!(!is_valid_did_method_webvh("did:webvh:", true));
860        assert!(!is_valid_did_method_webvh("did:webvh:abc123", true)); // missing content
861        assert!(!is_valid_did_method_webvh("did:webvh:abc123:", true)); // empty content
862        assert!(!is_valid_did_method_webvh("did:webvh::example.com", true)); // empty scim
863        assert!(!is_valid_did_method_webvh("did:webvh:example.com", true)); // no scim separator
864
865        // Invalid - scim contains invalid base58 characters
866        assert!(!is_valid_did_method_webvh(
867            "did:webvh:0abc:example.com",
868            true
869        )); // contains 0
870        assert!(!is_valid_did_method_webvh(
871            "did:webvh:Oabc:example.com",
872            true
873        )); // contains O
874        assert!(!is_valid_did_method_webvh(
875            "did:webvh:Iabc:example.com",
876            true
877        )); // contains I
878        assert!(!is_valid_did_method_webvh(
879            "did:webvh:labc:example.com",
880            true
881        )); // contains l
882        assert!(!is_valid_did_method_webvh(
883            "did:webvh:abc-123:example.com",
884            true
885        )); // contains -
886        assert!(!is_valid_did_method_webvh(
887            "did:webvh:abc_123:example.com",
888            true
889        )); // contains _
890        assert!(!is_valid_did_method_webvh(
891            "did:webvh:abc.123:example.com",
892            true
893        )); // contains .
894        assert!(!is_valid_did_method_webvh(
895            "did:webvh:abc@123:example.com",
896            true
897        )); // contains @
898        assert!(!is_valid_did_method_webvh(
899            "did:webvh:abc 123:example.com",
900            true
901        )); // contains space
902        assert!(!is_valid_did_method_webvh(
903            "did:webvh:abc!123:example.com",
904            true
905        )); // contains !
906        assert!(!is_valid_did_method_webvh(
907            "did:webvh:abc#123:example.com",
908            true
909        )); // contains #
910        assert!(!is_valid_did_method_webvh(
911            "did:webvh:abc$123:example.com",
912            true
913        )); // contains $
914
915        // Invalid - bad hostname in content
916        assert!(!is_valid_did_method_webvh("did:webvh:abc123:", false)); // empty hostname
917        assert!(!is_valid_did_method_webvh(
918            "did:webvh:abc123:..example.com",
919            true
920        ));
921        assert!(!is_valid_did_method_webvh(
922            "did:webvh:abc123:.example.com",
923            true
924        ));
925        assert!(!is_valid_did_method_webvh(
926            "did:webvh:abc123:example.com.",
927            true
928        ));
929        assert!(!is_valid_did_method_webvh(
930            "did:webvh:abc123:-example.com",
931            true
932        ));
933        assert!(!is_valid_did_method_webvh(
934            "did:webvh:abc123:example.localhost",
935            true
936        )); // reserved TLD
937        assert!(!is_valid_did_method_webvh(
938            "did:webvh:abc123:192.168.1.1",
939            true
940        )); // IPv4
941        assert!(!is_valid_did_method_webvh(
942            "did:webvh:abc123:2001:db8::1",
943            true
944        )); // IPv6
945
946        // Invalid in non-strict mode - empty path segments
947        assert!(!is_valid_did_method_webvh(
948            "did:webvh:abc123:example.com:",
949            false
950        ));
951        assert!(!is_valid_did_method_webvh(
952            "did:webvh:abc123:example.com::",
953            false
954        ));
955        assert!(!is_valid_did_method_webvh(
956            "did:webvh:abc123:example.com:path:",
957            false
958        ));
959        assert!(!is_valid_did_method_webvh(
960            "did:webvh:abc123:example.com::path",
961            false
962        ));
963
964        // Invalid in non-strict mode - non-alphanumeric in path segments
965        assert!(!is_valid_did_method_webvh(
966            "did:webvh:abc123:example.com:path/subpath",
967            false
968        ));
969        assert!(!is_valid_did_method_webvh(
970            "did:webvh:abc123:example.com:path-name",
971            false
972        ));
973        assert!(!is_valid_did_method_webvh(
974            "did:webvh:abc123:example.com:path_name",
975            false
976        ));
977        assert!(!is_valid_did_method_webvh(
978            "did:webvh:abc123:example.com:path.name",
979            false
980        ));
981        assert!(!is_valid_did_method_webvh(
982            "did:webvh:abc123:example.com:path@name",
983            false
984        ));
985        assert!(!is_valid_did_method_webvh(
986            "did:webvh:abc123:example.com:path name",
987            false
988        ));
989
990        // Edge cases with base58 characters
991        assert!(is_valid_did_method_webvh(
992            "did:webvh:111111:example.com",
993            true
994        )); // all 1s
995        assert!(is_valid_did_method_webvh(
996            "did:webvh:999999:example.com",
997            true
998        )); // all 9s
999        assert!(is_valid_did_method_webvh(
1000            "did:webvh:AAAAAA:example.com",
1001            true
1002        )); // all As
1003        assert!(is_valid_did_method_webvh(
1004            "did:webvh:zzzzzz:example.com",
1005            true
1006        )); // all zs
1007        assert!(is_valid_did_method_webvh(
1008            "did:webvh:HJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz:example.com",
1009            true
1010        )); // no excluded letters
1011    }
1012
1013    #[test]
1014    fn test_is_valid_base58_btc() {
1015        // Valid base58 strings
1016        assert!(is_valid_base58_btc("123456789"));
1017        assert!(is_valid_base58_btc(
1018            "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"
1019        ));
1020        assert!(is_valid_base58_btc("1"));
1021        assert!(is_valid_base58_btc("z"));
1022        assert!(is_valid_base58_btc("ABC123xyz"));
1023
1024        // Invalid - contains excluded characters
1025        assert!(!is_valid_base58_btc("0")); // zero
1026        assert!(!is_valid_base58_btc("O")); // capital O
1027        assert!(!is_valid_base58_btc("I")); // capital I
1028        assert!(!is_valid_base58_btc("l")); // lowercase l
1029        assert!(!is_valid_base58_btc("abc0def"));
1030        assert!(!is_valid_base58_btc("abcOdef"));
1031        assert!(!is_valid_base58_btc("abcIdef"));
1032        assert!(!is_valid_base58_btc("abcldef"));
1033
1034        // Invalid - contains non-alphanumeric characters
1035        assert!(!is_valid_base58_btc("abc-def"));
1036        assert!(!is_valid_base58_btc("abc_def"));
1037        assert!(!is_valid_base58_btc("abc.def"));
1038        assert!(!is_valid_base58_btc("abc@def"));
1039        assert!(!is_valid_base58_btc("abc def"));
1040        assert!(!is_valid_base58_btc("abc!def"));
1041        assert!(!is_valid_base58_btc(""));
1042
1043        // Edge cases
1044        assert!(is_valid_base58_btc("i")); // lowercase i is allowed
1045        assert!(is_valid_base58_btc("o")); // lowercase o is allowed
1046        assert!(is_valid_base58_btc("ioio")); // lowercase i and o are allowed
1047    }
1048}