Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions src/ip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1429,17 +1429,69 @@ describe('toLong() method', () => {
});

describe('fromLong() method', () => {
it('should repond with IPv4 address', () => {
it('should respond with IPv4 address', () => {
assert.equal(ip.fromLong(0x7f000001), '127.0.0.1');
assert.equal(ip.fromLong(0xffffffff), '255.255.255.255');
});

it('should reject out of bounds values', () => {
assert.throws(() => ip.fromLong(-1));
assert.throws(() => ip.fromLong(0xffffffff + 1));
});

it('should reject non-IP values', () => {
assert.throws(() => ip.fromLong(false as unknown as number));
assert.throws(() => ip.fromLong('foo' as unknown as number));
assert.throws(() => ip.fromLong([] as unknown as number));
assert.throws(() => ip.fromLong({} as unknown as number));
assert.throws(() => ip.fromLong(0xffffffff + 1));
});
});

describe('toBigInt() method', () => {
it('should respond with bigint values', () => {
assert.equal(ip.toBigInt('127.0.0.1'), 2130706433n);
assert.equal(ip.toBigInt('255.255.255.255'), 0xffffffffn);
});

it('should handle ipv6 addresses', () => {
assert.equal(ip.toBigInt('::1'), 1n);
assert.equal(ip.toBigInt('fd00::1'), 0xfd000000000000000000000000000001n);
});
});

describe('fromBigInt() method', () => {
it('should handle ipv4 addresses', () => {
assert.equal(ip.fromBigInt(2130706433n, 'ipv4'), '127.0.0.1');
});

it('should handle ipv6 addresses', () => {
assert.equal(
ip.fromBigInt(0xfd000000000000000000000000000001n, 'ipv6'),
'fd00::1',
);
});

it('should handle ipv6 addresses with leading zeros', () => {
assert.equal(ip.fromBigInt(1n, 'ipv6'), '::1');
});

it('should default to ipv6', () => {
assert.equal(ip.fromBigInt(1n), '::1');
});

it('should reject out of bounds values', () => {
assert.throws(() => ip.fromBigInt(-1n));
assert.throws(() => ip.fromBigInt(0xffffffffn + 1n, 'ipv4'));
assert.throws(() =>
ip.fromBigInt(0xffffffffffffffffffffffffffffffffn + 1n, 'ipv6'),
);
});

it('should reject non-IP values', () => {
assert.throws(() => ip.fromBigInt(false as unknown as bigint));
assert.throws(() => ip.fromBigInt('foo' as unknown as bigint));
assert.throws(() => ip.fromBigInt([] as unknown as bigint));
assert.throws(() => ip.fromBigInt({} as unknown as bigint));
});
});

Expand Down
70 changes: 62 additions & 8 deletions src/ip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import net from 'node:net';
* Returns true if the string looks like a number in octal format.
* @param byte The byte to test.
*/
const isOctal = (byte: string | number) => {
const isOctal = (byte: unknown) => {
const str = String(byte);
return str.startsWith('0') && /^[0-7]+$/.test(str);
};
Expand Down Expand Up @@ -92,7 +92,7 @@ const compressv6 = (words: string[]) => {
return '::';
}

// If the longest sequence is more than one zeros, then replace it with ''
// If the longest sequence is more than one zero, then replace it with ''
// Once joined with ':', the longest sequence will be '::'
if (longestZeroSequence.length > 1 && longestZeroSequence.start !== null) {
words.splice(longestZeroSequence.start, longestZeroSequence.length, '');
Expand Down Expand Up @@ -182,7 +182,7 @@ const v6toUInt8Array = (addr: string) => {
throw new Error('invalid ip address');
}

// Anything not a string is invalid
// Anything not a string is invalid
if (typeof addr !== 'string') {
throw new Error('invalid ip address');
}
Expand All @@ -208,7 +208,7 @@ const v6toUInt8Array = (addr: string) => {
const rightWords = parseWords(right);

if (leftWords.length + rightWords.length > SIXTEEN_BYTES) {
throw new Error('invalid ip address 3');
throw new Error('invalid ip address');
}

const padding = Array(SIXTEEN_BYTES).fill(0);
Expand Down Expand Up @@ -935,9 +935,9 @@ export const isPrivate = (addr: string | number) => {
/**
* Test if an IP address is a public address
* ```js
* isPrivate('127.0.0.1'); // fakse, loopback is private
* isPrivate('192.168.0.1'); // fakse, private network
* isPrivate('169.254.2.3'); // fakse, link-local is private
* isPrivate('127.0.0.1'); // false, loopback is private
* isPrivate('192.168.0.1'); // false, private network
* isPrivate('169.254.2.3'); // false, link-local is private
* isPrivate('8.8.8.8'); // true, google is public
* isPrivate('foo'); // false, invalid address
* ```
Expand Down Expand Up @@ -1058,7 +1058,7 @@ export const address = (
export const toLong = (addr: string) => {
const bytes = toUInt8Array(addr);
if (bytes.length !== 4) {
throw new Error('invalid ip address');
throw new Error('invalid ipv4 address');
}
return bytes.reduce((acc, byte) => acc * 256 + byte, 0);
};
Expand All @@ -1078,6 +1078,60 @@ export const fromLong = (int32: number) => {
return `${int32 >>> 24}.${(int32 >> 16) & 255}.${(int32 >> 8) & 255}.${int32 & 255}`;
};

/**
* Convert an IP address to a bigint
* ```js
* toBigInt('127.0.0.1'); // 2130706433n
* toBigInt('fd00::1'); // 336294682933583715844663186250927177729n
* ```
* @param addr The ip address to convert
* @throws {Error} If the address is invalid
*/
export const toBigInt = (addr: string) => {
const bytes = toUInt8Array(addr);
if (bytes.length !== 16 && bytes.length !== 4) {
throw new Error('invalid ip address');
}
return bytes.reduce((acc, byte) => acc * 256n + BigInt(byte), 0n);
};

/**
* Convert a 128-bit bigint to an IP address
* ```js
* fromBigInt(336294682933583715844663186250927177729n); // fd00::1
* fromBigInt(2130706433n); // 127.0.0.1
* ```
* @param int128 The bigint to convert
* @param family The IP family to use, defaults to ipv6
* @throws {Error} If the value is invalid
*/
export const fromBigInt = (int128: bigint, family: Family = 'ipv6') => {
family = normalizeFamily(family);
if (family !== 'ipv4' && family !== 'ipv6') {
throw new Error('family must be ipv4 or ipv6');
}

// For IPv4, we can use the existing fromLong function
if (family === 'ipv4') {
return fromLong(Number(int128));
}

// Otherwise, validate the bigint is in range for IPv6
if (int128 < 0n || int128 > 0xffffffffffffffffffffffffffffffffn) {
throw new Error('invalid bigint value');
}

const bytes = new Uint8Array(16);

let value = int128;
for (let i = 15; i >= 0; i -= 1) {
bytes[i] = Number(value & 0xffn);
value = value >> 8n;
}

return toString(bytes);
};

/**
* Convert an IP address to a 32-bit integer, returning -1 for invalid addresses
* ```js
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2015",
"target": "es2020",
"module": "esnext",
"lib": [],
"declaration": true,
Expand Down