diff --git a/src/index.test.ts b/src/index.test.ts index 142eb18..369d7ae 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -125,3 +125,13 @@ test("sanity check rangeOfNetworks IPv6", () => { "2001:440:ffff:ffff::/65" ]); }); + +test("sanity check sort", () => { + const output = index.sort(["192.168.2.3/31", "255.255.255.255", "192.168.0.0/16"], true); + expect(output).toEqual(["192.168.0.0/16", "192.168.2.3/31", "255.255.255.255/32"]); +}); + +test("sanity check summarize", () => { + const output = index.summarize(["192.168.0.0/16", "192.168.1.1", "192.168.2.3/31"], true); + expect(output).toEqual(["192.168.0.0/16"]); +}); diff --git a/src/index.ts b/src/index.ts index 32a728a..be39e8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -327,6 +327,108 @@ export function rangeOfNetworks(startAddress: string, stopAddress: string, throw return results; } +/** + * Sort returns an array of sorted networks + * + * @example + * netparser.sort(["255.255.255.255", "192.168.2.3/31", "192.168.0.0/16"]) // returns ["192.168.0.0/16", "192.168.2.3/31", "255.255.255.255/32"] + * + * @param networkAddresses - An array of addresses or subnets + * @param throwErrors - Stop the library from failing silently + * + * @returns An array of networks or null in case of error + */ +export function sort(networkAddresses: string[], throwErrors?: boolean) { + let subnets = new Array(networkAddresses.length) as shared.Network[]; + let foundCIDR = false; + for (let i = 0; i < networkAddresses.length; i++) { + const netString = networkAddresses[i]; + const addr = shared.parseAddressString(netString, throwErrors); + if (!addr) return null; + let cidr = shared.getCIDR(netString); + if (!cidr) { + if (addr.length == 4) { + cidr = 32; + } else { + cidr = 128; + } + } else { + foundCIDR = true; + } + subnets[i] = { bytes: addr, cidr: cidr }; + } + subnets = shared.sortNetworks(subnets); + const results = new Array(subnets.length) as string[]; + for (let i = 0; i < subnets.length; i++) { + let s = shared.bytesToAddr(subnets[i].bytes, throwErrors); + if (!s) return null; + results[i] = foundCIDR ? `${s}/${subnets[i].cidr}` : `${s}`; + } + return results; +} + +/** + * Summarize returns an array of aggregates given a list of networks + * + * @example + * netparser.summarize(["192.168.1.1", "192.168.0.0/16", "192.168.2.3/31"]) // returns ["192.168.0.0/16"] + * + * @param networks - An array of addresses or subnets + * @param strict - Do not automatically mask addresses to baseAddresses + * @param throwErrors - Stop the library from failing silently + * + * @returns An array of networks or null in case of error + */ +export function summarize(networks: string[], strict?: boolean, throwErrors?: boolean) { + let subnets = [] as shared.Network[]; + for (let i = 0; i < networks.length; i++) { + const netString = networks[i]; + let net = shared.parseNetworkString(netString, strict, false); + if (!net) { + const addr = shared.parseAddressString(netString, throwErrors); + if (!addr) return null; + if (addr.length == 4) { + net = { bytes: addr, cidr: 32 }; + } else { + net = { bytes: addr, cidr: 128 }; + } + if (subnets.length > 0 && subnets[0].bytes.length !== net.bytes.length) { + if (throwErrors) throw errors.MixingIPv4AndIPv6; + return null; + } + } + subnets[i] = net; + } + subnets = shared.sortNetworks(subnets); + const aggregates = [] as shared.Network[]; + for (let idx = 0; idx < subnets.length; idx++) { + aggregates.push(subnets[idx]); + let skipped = 0; + for (let i = idx + 1; i < subnets.length; i++) { + if (shared.networkContainsSubnet(subnets[idx], subnets[i])) { + skipped++; + continue; + } + if (subnets[idx].cidr === subnets[i].cidr) { + if (shared.networksAreAdjacent(subnets[idx], subnets[i])) { + subnets[idx].cidr--; + skipped++; + continue; + } + } + break; + } + idx += skipped; + } + const results = new Array(aggregates.length) as string[]; + for (let i = 0; i < aggregates.length; i++) { + let s = shared.bytesToAddr(aggregates[i].bytes, throwErrors); + if (!s) return null; + results[i] = `${s}/${aggregates[i].cidr}`; + } + return results; +} + module.exports = { baseAddress, broadcastAddress, @@ -339,7 +441,9 @@ module.exports = { networksIntersect, nextAddress, nextNetwork, - rangeOfNetworks + rangeOfNetworks, + sort, + summarize }; // The following functions are pending an implementation: diff --git a/src/shared.ts b/src/shared.ts index 091ecbd..5b0905b 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -235,7 +235,7 @@ export function networkContainsSubnet(net: Network, subnet: Network, throwErrors const netBytesEnd = duplicateAddress(net.bytes); if (!increaseAddressWithCIDR(netBytesEnd, net.cidr, throwErrors)) return false; const subnetBytesEnd = duplicateAddress(subnet.bytes); - if (!increaseAddressWithCIDR(subnet.bytes, subnet.cidr, throwErrors)) return false; + if (!increaseAddressWithCIDR(subnetBytesEnd, subnet.cidr, throwErrors)) return false; if (compareAddresses(netBytesEnd, subnetBytesEnd) < 0) return false; return true; } @@ -266,6 +266,14 @@ export function networksIntersect(net: Network, otherNet: Network, throwErrors?: return true; } +export function networksAreAdjacent(net: Network, otherNet: Network, throwErrors?: boolean) { + if (net.bytes.length !== otherNet.bytes.length) return false; + const netBytes = duplicateAddress(net.bytes); + if (!increaseAddressWithCIDR(netBytes, net.cidr, throwErrors)) return false; + if (compareAddresses(netBytes, otherNet.bytes) === 0) return true; + return false; +} + export function findNetworkIntersection(network: Network, otherNetworks: Network[]) { for (var otherNet of otherNetworks) { if (networksIntersect(network, otherNet)) { @@ -297,3 +305,104 @@ export function findNetworkWithoutIntersection(network: Network, otherNetworks: } return null; } + +enum IPVersion { + v4, + v6 +} + +function specificNetworks(networks: Network[], version: IPVersion) { + const results = [] as Network[]; + if (version === IPVersion.v4) { + for (let network of networks) { + if (network.bytes.length === 4) { + results.push(network); + } + } + } else if (version === IPVersion.v6) { + for (let network of networks) { + if (network.bytes.length === 16) { + results.push(network); + } + } + } + return results; +} + +function radixSortNetworks(networks: Network[], version: IPVersion) { + if (networks.length > 0 || version === IPVersion.v4 || version === IPVersion.v6) { + const counts = new Array(256) as number[]; + const offsetPrefixSum = new Array(256) as number[]; + const byteLength = version === IPVersion.v4 ? 4 : 16; + const maxCIDR = version === IPVersion.v4 ? 32 : 128; + + // in place swap and sort for every byte (including CIDR) + for (let byteIndex = 0; byteIndex <= byteLength; byteIndex++) { + for (let i = 0; i < counts.length; i++) { + counts[i] = 0; + } + + // count each occurance of byte value + for (let net of networks) { + if (byteIndex < byteLength) { + net.bytes[byteIndex] = Math.min(Math.max(0, net.bytes[byteIndex]), 255); + counts[net.bytes[byteIndex]]++; + } else { + net.cidr = Math.min(Math.max(0, net.cidr), maxCIDR); + counts[net.cidr]++; + } + } + + // initialize runningPrefixSum + let total = 0; + let oldCount = 0; + const runningPrefixSum = counts; + for (let i = 0; i < 256; i++) { + oldCount = counts[i]; + runningPrefixSum[i] = total; + total += oldCount; + } + + // initialize offsetPrefixSum (american flag sort) + for (let i = 0; i < 256; i++) { + if (i < 255) { + offsetPrefixSum[i] = runningPrefixSum[i + 1]; + } else { + offsetPrefixSum[i] = runningPrefixSum[i]; + } + } + + // in place swap and sort by value + let idx = 0; + let value = 0; + while (idx < networks.length) { + if (byteIndex < byteLength) { + value = networks[idx].bytes[byteIndex]; + } else { + value = networks[idx].cidr; + } + if (runningPrefixSum[value] !== idx) { + if (runningPrefixSum[value] < offsetPrefixSum[value]) { + let x = networks[runningPrefixSum[value]]; + networks[runningPrefixSum[value]] = networks[idx]; + networks[idx] = x; + } else { + idx++; + } + } else { + idx++; + } + runningPrefixSum[value]++; + } + } + } + return networks; +} + +export function sortNetworks(networks: Network[]) { + const v4 = specificNetworks(networks, IPVersion.v4); + const v6 = specificNetworks(networks, IPVersion.v6); + radixSortNetworks(v4, IPVersion.v4); + radixSortNetworks(v6, IPVersion.v6); + return [...v4, ...v6]; +}