From 93335f9952fe40eb0066b929eacb859b44b8a192 Mon Sep 17 00:00:00 2001 From: Alex Demskie Date: Wed, 8 May 2019 17:22:55 -0700 Subject: [PATCH 1/7] boilerplate starting point --- src/index.ts | 41 ++++++++++++++++++++++++++++++++++++++++- src/shared.ts | 8 ++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 32a728a..d3c5bc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -327,6 +327,44 @@ export function rangeOfNetworks(startAddress: string, stopAddress: string, throw 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 addrOrSubnetArray - 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(addrOrSubnetArray: string[], strict?: boolean, throwErrors?: boolean) { + let subnets = [] as shared.Network[]; + for (let netString of addrOrSubnetArray) { + 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.push(net); + } + subnets = shared.sortNetworks(subnets); + let largestSubnet = subnets.pop(); + for (let subnet of subnets) { + // TODO + } +} + module.exports = { baseAddress, broadcastAddress, @@ -339,7 +377,8 @@ module.exports = { networksIntersect, nextAddress, nextNetwork, - rangeOfNetworks + rangeOfNetworks, + summarize }; // The following functions are pending an implementation: diff --git a/src/shared.ts b/src/shared.ts index 091ecbd..cee543f 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -297,3 +297,11 @@ export function findNetworkWithoutIntersection(network: Network, otherNetworks: } return null; } + +export function sortNetworks(networks: Network[]) { + let arr = [] as Network[]; + if (networks.length > 0) { + // TODO + } + return arr; +} From b820a79cea10325023a65455a92c9b8745b7cb69 Mon Sep 17 00:00:00 2001 From: Alex Demskie Date: Fri, 10 May 2019 17:29:15 -0700 Subject: [PATCH 2/7] untested radix sort --- src/shared.ts | 74 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/src/shared.ts b/src/shared.ts index cee543f..2a51fcb 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -298,10 +298,74 @@ export function findNetworkWithoutIntersection(network: Network, otherNetworks: return null; } -export function sortNetworks(networks: Network[]) { - let arr = [] as Network[]; - if (networks.length > 0) { - // TODO +export enum IPVersion { + v4 = 4, + v6 = 6 +} + +export function sortNetworks(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, Math.floor(net.cidr), 255)); + counts[net.bytes[byteIndex]]++; + } else { + net.cidr = Math.min(Math.max(0, Math.floor(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] < offsetPrefixSum[value]) { + let idxOther = runningPrefixSum[value]; + let original = networks[idxOther]; + networks[idxOther] = networks[idx]; + networks[idx] = original; + offsetPrefixSum[value]++; + } else { + idx++; + } + } + } } - return arr; + return networks; } From 67f7f7bcb2801c941273b34f5a7f7905b61c33d6 Mon Sep 17 00:00:00 2001 From: Alex Demskie Date: Fri, 10 May 2019 17:59:59 -0700 Subject: [PATCH 3/7] making sortNetworks more generic --- src/shared.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/shared.ts b/src/shared.ts index 2a51fcb..60a8bb1 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -298,12 +298,30 @@ export function findNetworkWithoutIntersection(network: Network, otherNetworks: return null; } -export enum IPVersion { +enum IPVersion { v4 = 4, v6 = 6 } -export function sortNetworks(networks: Network[], version: IPVersion) { +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[]; @@ -369,3 +387,11 @@ export function sortNetworks(networks: Network[], version: IPVersion) { } 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]; +} From 7167c7a37a6cd5c2b0bce9fcdf1705e039eeb3c8 Mon Sep 17 00:00:00 2001 From: Alex Demskie Date: Sat, 11 May 2019 03:23:01 -0700 Subject: [PATCH 4/7] tested sort prototype --- src/index.test.ts | 5 +++++ src/index.ts | 50 +++++++++++++++++++++++++++++++++++++++++++---- src/shared.ts | 23 ++++++++++++---------- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 142eb18..28de197 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -125,3 +125,8 @@ 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"]); +}); diff --git a/src/index.ts b/src/index.ts index d3c5bc4..8160924 100644 --- a/src/index.ts +++ b/src/index.ts @@ -327,21 +327,62 @@ 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 addrOrSubnetArray - An array of addresses or subnets + * @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(addrOrSubnetArray: string[], strict?: boolean, throwErrors?: boolean) { +export function summarize(networks: string[], strict?: boolean, throwErrors?: boolean) { let subnets = [] as shared.Network[]; - for (let netString of addrOrSubnetArray) { + 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); @@ -356,7 +397,7 @@ export function summarize(addrOrSubnetArray: string[], strict?: boolean, throwEr return null; } } - subnets.push(net); + subnets[i] = net; } subnets = shared.sortNetworks(subnets); let largestSubnet = subnets.pop(); @@ -378,6 +419,7 @@ module.exports = { nextAddress, nextNetwork, rangeOfNetworks, + sort, summarize }; diff --git a/src/shared.ts b/src/shared.ts index 60a8bb1..0ebf69c 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -299,8 +299,8 @@ export function findNetworkWithoutIntersection(network: Network, otherNetworks: } enum IPVersion { - v4 = 4, - v6 = 6 + v4, + v6 } function specificNetworks(networks: Network[], version: IPVersion) { @@ -337,10 +337,10 @@ function radixSortNetworks(networks: Network[], version: IPVersion) { // count each occurance of byte value for (let net of networks) { if (byteIndex < byteLength) { - net.bytes[byteIndex] = Math.min(Math.max(0, Math.floor(net.cidr), 255)); + net.bytes[byteIndex] = Math.min(Math.max(0, net.bytes[byteIndex]), 255); counts[net.bytes[byteIndex]]++; } else { - net.cidr = Math.min(Math.max(0, Math.floor(net.cidr), maxCIDR)); + net.cidr = Math.min(Math.max(0, net.cidr), maxCIDR); counts[net.cidr]++; } } @@ -373,15 +373,18 @@ function radixSortNetworks(networks: Network[], version: IPVersion) { } else { value = networks[idx].cidr; } - if (runningPrefixSum[value] < offsetPrefixSum[value]) { - let idxOther = runningPrefixSum[value]; - let original = networks[idxOther]; - networks[idxOther] = networks[idx]; - networks[idx] = original; - offsetPrefixSum[value]++; + 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]++; } } } From a3e9bdadb2828c4f19bdc1d8720c23167871a60f Mon Sep 17 00:00:00 2001 From: Alex Demskie Date: Wed, 15 May 2019 02:14:23 -0700 Subject: [PATCH 5/7] implement networksAreAdjacent --- src/shared.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared.ts b/src/shared.ts index 0ebf69c..4d4be41 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -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)) { From 4ba9df0a9afb1d8db11118005478493ed6dfb7d7 Mon Sep 17 00:00:00 2001 From: Alex Demskie Date: Wed, 15 May 2019 02:15:47 -0700 Subject: [PATCH 6/7] first pass at summarize logic --- src/index.test.ts | 5 +++++ src/index.ts | 29 ++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 28de197..369d7ae 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -130,3 +130,8 @@ 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 8160924..be39e8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -400,10 +400,33 @@ export function summarize(networks: string[], strict?: boolean, throwErrors?: bo subnets[i] = net; } subnets = shared.sortNetworks(subnets); - let largestSubnet = subnets.pop(); - for (let subnet of subnets) { - // TODO + 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 = { From baa16755e983860a6b387da9df6399abb4c360ac Mon Sep 17 00:00:00 2001 From: Alex Demskie Date: Wed, 15 May 2019 02:16:58 -0700 Subject: [PATCH 7/7] stop networkContainsSubnet from mutating original bytes --- src/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared.ts b/src/shared.ts index 4d4be41..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; }