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 };
}