Release Network Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 11:38:57 +02:00
parent 729430295b
commit 429b55d587
30 changed files with 1474 additions and 96 deletions
+147
View File
@@ -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.",
],
};
}