impl/typescript.ts
1,769 bytes · the TypeScript implementation · view raw
import { type HostnameValidation } from "./net_hostname_validate_types.ts";
const ALLOWED = /^[A-Za-z0-9.-]*$/;
const DIGITS = /^[0-9]+$/;
/**
* Check a host name against the RFC 1123 / RFC 952 rules and normalise it:
* lower-case, one trailing dot (a fully qualified name) removed.
*
* Checks run in a fixed order and the first failure is the reason: empty,
* characters, total length, then each label in turn, then the top-level label.
*/
export function validateHostname(text: string): HostnameValidation {
if (typeof text !== "string") return { valid: false, hostname: null, reason: "not text", labels: 0 };
const name = text.endsWith(".") ? text.slice(0, -1) : text;
const labels = name.length === 0 ? [] : name.split(".");
const fail = (reason: string): HostnameValidation => ({ valid: false, hostname: null, reason, labels: labels.length });
if (name.length === 0) return fail("empty");
// Characters first: once the text is known to be ASCII, every language
// counts its length the same way.
if (!ALLOWED.test(name)) return fail("character other than a letter, digit, hyphen or dot");
if (name.length > 253) return fail("longer than 253 characters");
for (const label of labels) {
if (label.length === 0) return fail("empty label");
if (label.length > 63) return fail("label longer than 63 characters");
if (label.startsWith("-") || label.endsWith("-")) return fail("label starts or ends with a hyphen");
}
// RFC 3696 section 2: a top-level domain is never all-numeric, which keeps
// 1.2.3.4 an address rather than a name.
if (DIGITS.test(labels[labels.length - 1])) return fail("top-level label is all numeric");
return { valid: true, hostname: name.toLowerCase(), reason: null, labels: labels.length };
}