diff --git a/src/ip.test.ts b/src/ip.test.ts index 0c3114a..d784c81 100644 --- a/src/ip.test.ts +++ b/src/ip.test.ts @@ -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)); }); }); diff --git a/src/ip.ts b/src/ip.ts index 98bbe6f..a1e6a30 100644 --- a/src/ip.ts +++ b/src/ip.ts @@ -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); }; @@ -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, ''); @@ -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'); } @@ -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); @@ -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 * ``` @@ -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); }; @@ -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 diff --git a/tsconfig.json b/tsconfig.json index 84f7def..abb3ca4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2015", + "target": "es2020", "module": "esnext", "lib": [], "declaration": true,