import { formatIpv4, parseCidr } from "@add-ideas/toolbox-helpers"; const MAX_REQUIREMENTS = 1_024; const MAX_NAME_LENGTH = 120; export interface VlsmRequirement { name: string; hosts: number; } export interface VlsmAllocation extends VlsmRequirement { requestedIndex: number; prefixLength: number; network: string; firstUsable: string; lastUsable: string; broadcast: string; usableHosts: number; addresses: number; } export interface VlsmPlan { base: string; allocations: VlsmAllocation[]; usedAddresses: number; freeAddresses: number; warnings: string[]; } function prefixForHosts(hosts: number): number { if (!Number.isSafeInteger(hosts) || hosts < 1 || hosts > 4_294_967_294) throw new Error( "Host requirements must be integers from 1 to 4,294,967,294.", ); const addressBits = Math.ceil(Math.log2(hosts + 2)); return 32 - Math.max(2, addressBits); } function align(value: bigint, block: bigint): bigint { const remainder = value % block; return remainder === 0n ? value : value + block - remainder; } export function parseVlsmRequirements(source: string): VlsmRequirement[] { if (source.length > 256 * 1024) throw new Error("Requirements exceed the 256 KiB limit."); const requirements: VlsmRequirement[] = []; for (const [index, raw] of source .replaceAll("\r\n", "\n") .split("\n") .entries()) { const line = raw.trim(); if (!line || line.startsWith("#")) continue; if (requirements.length >= MAX_REQUIREMENTS) throw new Error( `More than ${MAX_REQUIREMENTS} subnet requirements were supplied.`, ); const separator = line.lastIndexOf(","); if (separator < 1) throw new Error(`Line ${index + 1}: expected name,hosts.`); const name = line.slice(0, separator).trim(); const hostsText = line.slice(separator + 1).trim(); if (!name || name.length > MAX_NAME_LENGTH) throw new Error( `Line ${index + 1}: name must contain 1–${MAX_NAME_LENGTH} characters.`, ); if (!/^\d+$/u.test(hostsText)) throw new Error(`Line ${index + 1}: hosts must be a positive integer.`); const hosts = Number(hostsText); prefixForHosts(hosts); requirements.push({ name, hosts }); } if (requirements.length === 0) throw new Error("Enter at least one name,hosts requirement."); return requirements; } export function planVlsm( baseCidr: string, requirements: readonly VlsmRequirement[], ): VlsmPlan { if (requirements.length === 0 || requirements.length > MAX_REQUIREMENTS) throw new Error( `Supply between 1 and ${MAX_REQUIREMENTS} subnet requirements.`, ); const base = parseCidr(baseCidr); if (base.version !== 4) throw new Error( "The VLSM planner currently supports IPv4 base networks only.", ); const sorted = requirements .map((requirement, requestedIndex) => ({ ...requirement, requestedIndex, prefixLength: prefixForHosts(requirement.hosts), })) .sort( (left, right) => left.prefixLength - right.prefixLength || left.requestedIndex - right.requestedIndex, ); let cursor = base.network.value; const endExclusive = base.network.value + base.size; const allocations: VlsmAllocation[] = []; for (const requirement of sorted) { if (requirement.prefixLength < base.prefixLength) throw new Error( `${requirement.name} needs /${requirement.prefixLength}, which is larger than ${base.canonical}.`, ); const addresses = 2 ** (32 - requirement.prefixLength); const block = BigInt(addresses); const network = align(cursor, block); const after = network + block; if (after > endExclusive) throw new Error( `${requirement.name} does not fit. Allocation stopped before producing a partial plan.`, ); allocations.push({ name: requirement.name, hosts: requirement.hosts, requestedIndex: requirement.requestedIndex, prefixLength: requirement.prefixLength, network: `${formatIpv4(network)}/${requirement.prefixLength}`, firstUsable: formatIpv4(network + 1n), lastUsable: formatIpv4(after - 2n), broadcast: formatIpv4(after - 1n), usableHosts: addresses - 2, addresses, }); cursor = after; } const used = allocations.reduce((total, item) => total + item.addresses, 0); const baseSize = Number(base.size); return { base: base.canonical, allocations, usedAddresses: used, freeAddresses: baseSize - used, warnings: [ "Allocations are largest-first and use conventional network/broadcast reservations; point-to-point /31 semantics are not assumed.", "Free-address count includes alignment gaps and is not itself a contiguous remainder guarantee.", ], }; }