From 6f105d5e630bbeff350e475a5f64959c00aa0dee Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Tue, 21 Jul 2026 09:59:57 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20did=20hostname?= =?UTF-8?q?=20parsing=20into=20separate=20module=20for=20reuse.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/at/internal/hostname.gleam | 156 +++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/at/internal/hostname.gleam diff --git a/src/at/internal/hostname.gleam b/src/at/internal/hostname.gleam new file mode 100644 index 0000000..7aa814e --- /dev/null +++ b/src/at/internal/hostname.gleam @@ -0,0 +1,156 @@ +// TYPES ----------------------------------------------------------------------- + +/// +/// +pub type Tld { + Normal + Reserved + Test +} + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// Validate a lowercase ASCII hostname against some common syntax rules outlined +/// in the atproto spec. Specifically atproto [handles](https://atproto.com/specs/handle#handle-identifier-syntax) +/// and [dids](https://atproto.com/specs/did#did-identifier-syntax): +/// +/// - made up of 2 or more segments, separated by `.` +/// +/// - each segment must be 1-63 characters long +/// +/// - the overall hostname (including separating periods) must be at most 253 +/// characters long +/// +/// - segments may only contain ASCII digits, lowercase letters, and hyphens, +/// and cannot start or end with a hyphen +/// +/// - the last segment (the TLD) cannot start with a digit +/// +/// The `.invalid` TLD is always rejected: atproto reserves it exclusively for +/// the special `handle.invalid` value, which callers should special-case +/// themselves before calling this function. +/// +/// > *Note*: the spec sometimes calls for normalised input and sometimes doesn't. +/// > This function assumes the input is already normalised (if necessary) and +/// > will reject capital letters like `"A"`. +/// +pub fn parse(hostname: String) -> Result(Tld, Nil) { + do_parse(hostname, "", 0, 1, 0, False) +} + +fn do_parse( + remaining: String, + tld: String, + host_length: Int, + segment_count: Int, + segment_length: Int, + trailing_hyphen: Bool, +) -> Result(Tld, Nil) { + case remaining { + // hostnames cannot be longer than 253 characters + _ if host_length > 253 -> Error(Nil) + + // a single segment cannot be longer than 63 characters + _ if segment_length > 63 -> Error(Nil) + + "" if segment_count >= 2 && segment_length > 0 && !trailing_hyphen -> + is_valid_tld(tld) + + // segments cannot be empty or begin with a hyphen + "." <> _ if segment_length == 0 -> Error(Nil) + "-" <> _ if segment_length == 0 -> Error(Nil) + + // a dot starts a new segment + "." <> rest if !trailing_hyphen -> + do_parse(rest, rest, host_length + 1, segment_count + 1, 0, False) + + // a hyphen is valid inside a segment, but cannot be its final character + "-" <> rest -> + do_parse( + rest, + tld, + host_length + 1, + segment_count, + segment_length + 1, + True, + ) + + // lowercase ascii and digits are valid + "0" <> rest + | "1" <> rest + | "2" <> rest + | "3" <> rest + | "4" <> rest + | "5" <> rest + | "6" <> rest + | "7" <> rest + | "8" <> rest + | "9" <> rest + | "a" <> rest + | "b" <> rest + | "c" <> rest + | "d" <> rest + | "e" <> rest + | "f" <> rest + | "g" <> rest + | "h" <> rest + | "i" <> rest + | "j" <> rest + | "k" <> rest + | "l" <> rest + | "m" <> rest + | "n" <> rest + | "o" <> rest + | "p" <> rest + | "q" <> rest + | "r" <> rest + | "s" <> rest + | "t" <> rest + | "u" <> rest + | "v" <> rest + | "w" <> rest + | "x" <> rest + | "y" <> rest + | "z" <> rest -> + do_parse( + rest, + tld, + host_length + 1, + segment_count, + segment_length + 1, + False, + ) + + // everything else isn't. + _ -> Error(Nil) + } +} + +fn is_valid_tld(tld: String) -> Result(Tld, Nil) { + case tld { + // a tld cannot start with a digit + "0" <> _ + | "1" <> _ + | "2" <> _ + | "3" <> _ + | "4" <> _ + | "5" <> _ + | "6" <> _ + | "7" <> _ + | "8" <> _ + | "9" <> _ -> Error(Nil) + + // the `.invalid` tld is reserved for the special `handle.invalid` hostname + // in the case of atproto handles and should fail in general. + "invalid" -> Error(Nil) + + // these tlds are reserved by atproto for special use cases or for local + // development. + "alt" | "arpa" | "example" | "internal" | "local" | "localhost" | "onion" -> + Ok(Reserved) + + "test" -> Ok(Test) + + _ -> Ok(Normal) + } +} -- 2.51.2