Functional Weave
Code in Rust

net.device-validate@1.0.0

impl/typescript.ts

4,582 bytes · the TypeScript implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { isCidr, cidrInfo } from "./net_cidr.ts";  ← from net.cidr ^1.0.0 · built alongside by fune
import { validateHostname } from "./net_hostname_validate.ts";  ← from net.hostname-validate ^1.0.0 · built alongside by fune
import { isIpv4 } from "./net_ipv4.ts";  ← from net.ipv4 ^1.0.0 · built alongside by fune
import { formatIpv6, isIpv6, parseIpv6 } from "./net_ipv6.ts";  ← from net.ipv6 ^1.0.0 · built alongside by fune
import { validateMacAddress } from "./net_mac_address.ts";  ← from net.mac-address ^1.0.0 · built alongside by fune
import { isPort } from "./net_port.ts";  ← from net.port ^1.0.0 · built alongside by fune
import { type DeviceValidation } from "./net_device_validate_types.ts";

const KNOWN = ["name", "host", "ports", "mac", "subnet"];

const present = (entry: Readonly<Record<string, unknown>>, key: string): boolean =>
  Object.prototype.hasOwnProperty.call(entry, key) && entry[key] !== null && entry[key] !== undefined;

/** Trim ASCII whitespace only: String.prototype.trim also strips Unicode spaces, which Python and Rust here do not. */
const trimAscii = (text: string): string => text.replace(/^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g, "");

/** A whole number, whichever way the JSON reader stored it (22 and 22.0 are the same port). */
const wholeNumber = (value: unknown): number | null =>
  typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) ? value : null;

/** An IPv6 block with its host bits cleared and its address in RFC 5952 form. */
function canonicalIpv6Block(text: string): string {
  const slash = text.indexOf("/");
  const prefix = Number(text.slice(slash + 1));
  const groups = parseIpv6(text.slice(0, slash)).map((g, i) => {
    const bits = Math.min(16, Math.max(0, prefix - 16 * i));
    return bits === 0 ? 0 : g & ((0xffff << (16 - bits)) & 0xffff);
  });
  return `${formatIpv6(groups)}/${prefix}`;
}

/**
 * Check one inventory entry and say everything that is wrong with it at once,
 * so a person fixing a file does not play whack-a-mole one error per run.
 * Never throws: a bad entry is a result, not a crash.
 */
export function validateDevice(entry: Readonly<Record<string, unknown>>): DeviceValidation {
  const errors: string[] = [];

  let name: string | null = null;
  if (!present(entry, "name")) errors.push("name is required");
  else if (typeof entry.name !== "string") errors.push("name must be text");
  else if (trimAscii(entry.name) === "") errors.push("name must not be empty");
  else name = trimAscii(entry.name);

  let host: string | null = null;
  let hostKind: string | null = null;
  if (!present(entry, "host")) errors.push("host is required");
  else if (typeof entry.host !== "string") errors.push("host must be text");
  else if (isIpv4(entry.host)) {
    host = entry.host;
    hostKind = "ipv4";
  } else if (isIpv6(entry.host)) {
    host = formatIpv6(parseIpv6(entry.host));
    hostKind = "ipv6";
  } else {
    const check = validateHostname(entry.host);
    if (check.valid) {
      host = check.hostname;
      hostKind = "hostname";
    } else {
      errors.push(`host "${entry.host}" is not an IPv4 address, IPv6 address or host name: ${check.reason}`);
    }
  }

  const ports: number[] = [];
  if (!present(entry, "ports")) errors.push("ports is required");
  else if (!Array.isArray(entry.ports)) errors.push("ports must be a list");
  else if (entry.ports.length === 0) errors.push("ports must not be empty");
  else {
    entry.ports.forEach((item: unknown, i: number) => {
      const port = wholeNumber(item);
      if (port === null) errors.push(`ports[${i}] must be a whole number`);
      else if (!isPort(port)) errors.push(`ports[${i}] ${port} is not a port (1-65535)`);
      else if (ports.includes(port)) errors.push(`ports[${i}] ${port} is listed twice`);
      else ports.push(port);
    });
  }

  let mac: string | null = null;
  if (present(entry, "mac")) {
    if (typeof entry.mac !== "string") errors.push("mac must be text");
    else {
      const check = validateMacAddress(entry.mac);
      if (check.valid) mac = check.canonical;
      else errors.push(`mac "${entry.mac}" is not valid: ${check.reason}`);
    }
  }

  let subnet: string | null = null;
  if (present(entry, "subnet")) {
    if (typeof entry.subnet !== "string") errors.push("subnet must be text");
    else if (!isCidr(entry.subnet)) errors.push(`subnet "${entry.subnet}" is not a CIDR block`);
    else subnet = entry.subnet.includes(":") ? canonicalIpv6Block(entry.subnet) : cidrInfo(entry.subnet).cidr;
  }

  // Sorted, not in file order: JavaScript lists integer-like keys first, so
  // "file order" would differ between languages.
  const unknown = Object.keys(entry).filter((k) => !KNOWN.includes(k)).sort();
  for (const key of unknown) errors.push(`unknown field "${key}"`);

  return { valid: errors.length === 0, errors, name, host, hostKind, ports, mac, subnet };
}