+519
-15
crates/atproto-identity/src/validation.rs
+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
}