148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
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.",
|
||
],
|
||
};
|
||
}
|