atproto_identity/
validation.rs1const MAX_HOSTNAME_LENGTH: usize = 253;
9
10const MAX_LABEL_LENGTH: usize = 63;
12
13const RESERVED_TLDS: [&str; 4] = [".localhost", ".internal", ".arpa", ".local"];
15
16pub fn is_valid_hostname(hostname: &str) -> bool {
19 if hostname.is_empty() || hostname.len() > MAX_HOSTNAME_LENGTH {
21 return false;
22 }
23
24 if RESERVED_TLDS.iter().any(|tld| hostname.ends_with(tld)) {
26 return false;
27 }
28
29 if hostname.bytes().any(|byte| !is_valid_hostname_char(byte)) {
31 return false;
32 }
33
34 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
57pub fn is_valid_handle(handle: &str) -> Option<String> {
60 let trimmed = strip_handle_prefixes(handle);
62
63 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
81pub 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}