@@ -0,0 +1,147 @@
|
||||
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.",
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user