Created
April 13, 2018 05:42
-
-
Save NickHeiner/62e4bef78576ea2fb645fbfaef247d0d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Run with Node.js v9.11.1 | |
const _ = require('lodash'); | |
const partsOfIpAddr = 4; | |
const bitsPerIpPart = 8; | |
const numberBitLength = partsOfIpAddr * bitsPerIpPart; | |
const getValueFromFlippingTrailingZeros = countZeros => Math.pow(2, countZeros) - 1; | |
const getCountOfChangeableTrailingZeros = (num, upperBound) => { | |
let countZeros = 0; | |
for (; (1 << countZeros) < num; countZeros++) { | |
const valueFromFlippingTrailingZeros = getValueFromFlippingTrailingZeros(countZeros); | |
if (num & (1 << countZeros) || num + valueFromFlippingTrailingZeros >= upperBound) { | |
return countZeros; | |
} | |
} | |
return countZeros; | |
}; | |
const getShiftAmountForIpPart = partIndex => (partsOfIpAddr - partIndex - 1) * bitsPerIpPart; | |
const numberOfIp = ip => { | |
const ipParts = ip.split('.'); | |
return _(ipParts) | |
.map(part => parseInt(part)) | |
.map((part, index) => { | |
const shiftAmount = getShiftAmountForIpPart(index); | |
return part << shiftAmount; | |
}) | |
.sum(); | |
}; | |
const ipOfNumber = ipNumber => { | |
const bitmask = Math.pow(2, bitsPerIpPart) - 1; | |
return _(partsOfIpAddr) | |
.range() | |
.map(partIndex => { | |
const shiftAmount = getShiftAmountForIpPart(partIndex); | |
const shiftedBitMask = bitmask << shiftAmount; | |
const maskedIpNumber = shiftedBitMask & ipNumber; | |
return (maskedIpNumber >> shiftAmount).toString(); | |
}) | |
.join('.'); | |
}; | |
// Note: JS bitwise operators treat numbers as being 32 bit, but the top bit is used for the sign. | |
// Thus, 127.255.255.255 is the largest IP address that this code can handle. | |
const maxIpValue = numberOfIp('127.255.255.255'); | |
const getCidrRangeNumber = (ipNumber, range) => { | |
if (!range) { | |
return [`${ipOfNumber(ipNumber)}/${numberBitLength}`]; | |
} | |
const upperBound = ipNumber + range; | |
if (upperBound > maxIpValue) { | |
throw new Error( | |
`${ipNumber} (${ipOfNumber(ipNumber)}) + range ${range} would exceed the max IP value of ${maxIpValue}` | |
); | |
} | |
const countChangeableTrailingZeros = getCountOfChangeableTrailingZeros(ipNumber, upperBound); | |
const valueByChangingTrailingZeros = ipNumber + getValueFromFlippingTrailingZeros(countChangeableTrailingZeros); | |
const nextLowerBound = Math.min(upperBound, valueByChangingTrailingZeros + 1); | |
const nextRange = upperBound - nextLowerBound; | |
const shouldContinue = valueByChangingTrailingZeros + 1 <= upperBound; | |
const rest = shouldContinue ? getCidrRangeNumber(nextLowerBound, nextRange) : []; | |
const cidrRangeNotation = numberBitLength - countChangeableTrailingZeros; | |
return [`${ipOfNumber(ipNumber)}/${cidrRangeNotation}`].concat(rest); | |
}; | |
const getCidrRange = (ip, range) => { | |
const ipNumber = numberOfIp(ip); | |
return getCidrRangeNumber(ipNumber, range); | |
}; | |
[ | |
{fn: numberOfIp, input: ['0.0.0.0'], expected: 0}, | |
{fn: numberOfIp, input: ['0.0.0.1'], expected: 1}, | |
{fn: numberOfIp, input: ['0.0.0.8'], expected: 8}, | |
{fn: numberOfIp, input: ['0.0.1.0'], expected: Math.pow(2, 8)}, | |
{fn: numberOfIp, input: ['0.1.1.0'], expected: Math.pow(2, 8) + Math.pow(2, 16)}, | |
{fn: numberOfIp, input: ['1.0.0.0'], expected: Math.pow(2, 24)}, | |
{fn: ipOfNumber, expected: '0.0.0.0', input: [0]}, | |
{fn: ipOfNumber, expected: '0.0.0.1', input: [1]}, | |
{fn: ipOfNumber, expected: '0.0.0.8', input: [8]}, | |
{fn: ipOfNumber, expected: '0.0.1.0', input: [Math.pow(2, 8)]}, | |
{fn: ipOfNumber, expected: '0.1.1.0', input: [Math.pow(2, 8) + Math.pow(2, 16)]}, | |
{fn: ipOfNumber, expected: '1.0.0.0', input: [Math.pow(2, 24)]}, | |
{fn: getCountOfChangeableTrailingZeros, input: [16, 17], expected: 1}, | |
{input: ['0.0.0.5', 3], expected: ['0.0.0.5/32', '0.0.0.6/31', '0.0.0.8/32'], fn: getCidrRange}, | |
{input: ['10.0.0.5', 3], expected: ['10.0.0.5/32', '10.0.0.6/31', '10.0.0.8/32'], fn: getCidrRange}, | |
{input: ['0.0.0.5', 17 - 5], expected: [ | |
'0.0.0.5/32', | |
'0.0.0.6/31', | |
'0.0.0.8/29', | |
'0.0.0.16/31' | |
], fn: getCidrRange}, | |
// TODO: This test does not pass. | |
// {input: ['0.0.10.10', numberOfIp('0.0.20.0') - numberOfIp('0.0.10.10')], expected: [ | |
// '0.0.10.10/31', | |
// '0.0.10.12/30', | |
// '0.0.10.16/28', | |
// '0.0.10.32/27', | |
// '0.0.10.64/26', | |
// '0.0.10.128/25', | |
// '0.0.11.0/24', | |
// '0.0.12.0/22', | |
// '0.0.16.0/22', | |
// '0.0.20.0/32' | |
// ], fn: getCidrRange} | |
].forEach(({input, expected, fn}) => { | |
const actual = fn(...input); | |
if (_.isEqual(actual, expected)) { | |
console.log('Test passed'); | |
return; | |
} | |
console.log('Test failed', {actual, expected}); | |
}); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment