atproto_identity/
validation.rs

1//! Input validation for AT Protocol handles and DIDs.
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.
6
7/// Maximum length for a valid hostname as defined in RFC 1035
8const MAX_HOSTNAME_LENGTH: usize = 253;
9
10/// Maximum length for a DNS label (component between dots) as defined in RFC 1035
11const MAX_LABEL_LENGTH: usize = 63;
12
13/// List of reserved top-level domains that are not valid for AT Protocol handles
14const RESERVED_TLDS: [&str; 4] = [".localhost", ".internal", ".arpa", ".local"];
15
16/// Validates if a string is a valid hostname according to RFC 1035.
17/// Checks length limits, reserved TLDs, and character validity.
18pub fn is_valid_hostname(hostname: &str) -> bool {
19    // Empty hostnames are invalid
20    if hostname.is_empty() || hostname.len() > MAX_HOSTNAME_LENGTH {
21        return false;
22    }
23
24    // Check if hostname uses any reserved TLDs
25    if RESERVED_TLDS.iter().any(|tld| hostname.ends_with(tld)) {
26        return false;
27    }
28
29    // Ensure all characters are valid hostname characters
30    if hostname.bytes().any(|byte| !is_valid_hostname_char(byte)) {
31        return false;
32    }
33
34    // Validate each DNS label in the hostname
35    if hostname.split('.').any(|label| !is_valid_dns_label(label)) {
36        return false;
37    }
38
39    true
40}
41
42fn is_valid_hostname_char(byte: u8) -> bool {
43    byte.is_ascii_lowercase()
44        || byte.is_ascii_uppercase()
45        || byte.is_ascii_digit()
46        || byte == b'-'
47        || byte == b'.'
48}
49
50fn is_valid_dns_label(label: &str) -> bool {
51    !(label.is_empty()
52        || label.len() > MAX_LABEL_LENGTH
53        || label.starts_with('-')
54        || label.ends_with('-'))
55}
56
57/// Validates and normalizes an AT Protocol handle.
58/// Returns the normalized handle if valid, None otherwise.
59pub fn is_valid_handle(handle: &str) -> Option<String> {
60    // Strip optional prefixes to get the core handle
61    let trimmed = strip_handle_prefixes(handle);
62
63    // A valid handle must be a valid hostname with at least one period
64    if is_valid_hostname(trimmed) && trimmed.contains('.') {
65        Some(trimmed.to_string())
66    } else {
67        None
68    }
69}
70
71fn strip_handle_prefixes(handle: &str) -> &str {
72    if let Some(value) = handle.strip_prefix("at://") {
73        value
74    } else if let Some(value) = handle.strip_prefix('@') {
75        value
76    } else {
77        handle
78    }
79}
80
81/// Validates if a string is a properly formatted PLC DID.
82/// Checks for correct prefix and 24-character identifier length.
83pub fn is_valid_did_method_plc(did: &str) -> bool {
84    let did_value = match did.strip_prefix("did:plc:") {
85        Some(value) => value,
86        None => return false,
87    };
88
89    did_value.len() == 24
90}