// Character predicates, spelled out rather than taken from a Unicode table.
//
// Every atproto identifier syntax is ASCII-only, and each of these validators
// gates on the allowed character set before it does anything else. That gate is
// what makes the rest of the code safe to reason about in UTF-16 units: once a
// string is known to be ASCII, `length()` is the character count, `substring`
// cannot split a surrogate pair, and the byte length equals both.
//
// The gate is therefore load-bearing, not a fast path. Removing it would make
// every subsequent length check quietly wrong for non-ASCII input.
///|
fn is_ascii_lower(c : Char) -> Bool {
c >= 'a' && c <= 'z'
}
///|
fn is_ascii_alpha(c : Char) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
///|
fn is_ascii_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn is_ascii_alnum(c : Char) -> Bool {
is_ascii_alpha(c) || is_ascii_digit(c)
}
///|
fn all_chars(s : String, pred : (Char) -> Bool) -> Bool {
for c in s.iter() {
if !pred(c) {
return false
}
}
true
}
///|
/// `[a-zA-Z0-9.-]`, the character set shared by handles and NSIDs.
fn is_dns_char(c : Char) -> Bool {
is_ascii_alnum(c) || c == '.' || c == '-'
}
// Indexing a string -- `s[0]`, `s[s.length() - 1]` -- yields a UTF-16 code
// unit, not a `Char`. Once the ASCII gate above has run the two coincide, so
// the first/last-character checks index directly instead of decoding, and these
// two helpers spell the comparison in terms of a character literal so the code
// still reads as the rule it implements.
///|
fn code_unit_is(u : UInt16, c : Char) -> Bool {
u.to_int() == c.to_int()
}
///|
fn code_unit_is_ascii_alpha(u : UInt16) -> Bool {
let i = u.to_int()
(i >= 'a'.to_int() && i <= 'z'.to_int()) ||
(i >= 'A'.to_int() && i <= 'Z'.to_int())
}
///|
fn code_unit_is_ascii_digit(u : UInt16) -> Bool {
let i = u.to_int()
i >= '0'.to_int() && i <= '9'.to_int()
}