Skip to content

Instantly share code, notes, and snippets.

@NickHeiner
Created April 13, 2018 05:42
Show Gist options
  • Save NickHeiner/62e4bef78576ea2fb645fbfaef247d0d to your computer and use it in GitHub Desktop.
Save NickHeiner/62e4bef78576ea2fb645fbfaef247d0d to your computer and use it in GitHub Desktop.
// 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