Skip to content

Commit 95af5f1

Browse files
committed
feat: expanded IP address format conversion functions
* added conversions from V4 to V6 mapped, both Dec and hex types. * Added isX for mappedV4 dec and hex. added a helper toCanonicalIp to convert all format types to the canonical form. * Fixes #18 [ci skip]
1 parent d542dcb commit 95af5f1

File tree

2 files changed

+118
-9
lines changed

2 files changed

+118
-9
lines changed

src/utils.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,52 @@ function isIPv4MappedIPv6(host: string): host is Host {
5454
return false;
5555
}
5656

57+
function isIPv4MappedIPv6Hex(host: string): host is Host {
58+
if (host.startsWith('::ffff:')) {
59+
try {
60+
// The `ip-num` package understands `::ffff:7f00:1`
61+
IPv6.fromString(host);
62+
return true;
63+
} catch {
64+
return false;
65+
}
66+
}
67+
return false;
68+
}
69+
70+
function isIPv4MappedIPv6Dec(host: string): host is Host {
71+
if (host.startsWith('::ffff:')) {
72+
// But it does not understand `::ffff:127.0.0.1`
73+
const ipv4 = host.slice('::ffff:'.length);
74+
if (isIPv4(ipv4)) {
75+
return true;
76+
}
77+
}
78+
return false;
79+
}
80+
5781
/**
5882
* Takes an IPv4 address and returns the IPv4 mapped IPv6 address.
5983
* This produces the dotted decimal variant.
6084
*/
61-
function toIPv4MappedIPv6(host: string): Host {
85+
function toIPv4MappedIPv6Dec(host: string): Host {
6286
if (!isIPv4(host)) {
6387
throw new TypeError('Invalid IPv4 address');
6488
}
6589
return ('::ffff:' + host) as Host;
6690
}
6791

92+
/**
93+
* Takes an IPv4 address and returns the IPv4 mapped IPv6 address.
94+
* This produces the dotted Hexidecimal variant.
95+
*/
96+
function toIPv4MappedIPv6Hex(host: string): Host {
97+
if (!isIPv4(host)) {
98+
throw new TypeError('Invalid IPv4 address');
99+
}
100+
return IPv4.fromString(host).toIPv4MappedIPv6().toString() as Host;
101+
}
102+
68103
/**
69104
* Extracts the IPv4 portion out of the IPv4 mapped IPv6 address.
70105
* Can handle both the dotted decimal and hex variants.
@@ -87,6 +122,20 @@ function fromIPv4MappedIPv6(host: string): Host {
87122
return ipv4Decs.join('.') as Host;
88123
}
89124

125+
/**
126+
* This converts all `IPv4` formats to the `IPv4` decimal format.
127+
* `IPv4` decimal and `IPv6` hex formatted IPs are left unchanged.
128+
*/
129+
function toCanonicalIp(host: string) {
130+
if (isIPv4MappedIPv6(host)) {
131+
return fromIPv4MappedIPv6(host);
132+
}
133+
if (isIPv4(host) || isIPv6(host)) {
134+
return host;
135+
}
136+
throw new TypeError('Invalid IP address');
137+
}
138+
90139
/**
91140
* This will resolve a hostname to the first host.
92141
* It could be an IPv6 address or IPv4 address.
@@ -226,7 +275,7 @@ function resolvesZeroIP(host: Host): Host {
226275
if (isIPv4MappedIPv6(host)) {
227276
const ipv4 = fromIPv4MappedIPv6(host);
228277
if (new IPv4(ipv4).isEquals(zeroIPv4)) {
229-
return toIPv4MappedIPv6('127.0.0.1');
278+
return toIPv4MappedIPv6Dec('127.0.0.1');
230279
} else {
231280
return host;
232281
}
@@ -273,8 +322,12 @@ export {
273322
isIPv4,
274323
isIPv6,
275324
isIPv4MappedIPv6,
276-
toIPv4MappedIPv6,
325+
isIPv4MappedIPv6Hex,
326+
isIPv4MappedIPv6Dec,
327+
toIPv4MappedIPv6Dec,
328+
toIPv4MappedIPv6Hex,
277329
fromIPv4MappedIPv6,
330+
toCanonicalIp,
278331
resolveHostname,
279332
resolveHost,
280333
promisify,

tests/utils.test.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,45 @@ describe('utils', () => {
1212
expect(utils.isIPv4MappedIPv6('::ffff:7f00:800')).toBe(true);
1313
expect(utils.isIPv4MappedIPv6('::ffff:255.255.255.255')).toBe(true);
1414
});
15-
test('to and from IPv4 mapped IPv6 addresses', () => {
16-
expect(utils.toIPv4MappedIPv6('127.0.0.1')).toBe('::ffff:127.0.0.1');
17-
expect(utils.toIPv4MappedIPv6('0.0.0.0')).toBe('::ffff:0.0.0.0');
18-
expect(utils.toIPv4MappedIPv6('255.255.255.255')).toBe(
15+
test('detect IPv4 mapped IPv6 Dec addresses', () => {
16+
expect(utils.isIPv4MappedIPv6Dec('::ffff:127.0.0.1')).toBe(true);
17+
expect(utils.isIPv4MappedIPv6Dec('::ffff:7f00:1')).toBe(false);
18+
expect(utils.isIPv4MappedIPv6Dec('::')).toBe(false);
19+
expect(utils.isIPv4MappedIPv6Dec('::1')).toBe(false);
20+
expect(utils.isIPv4MappedIPv6Dec('127.0.0.1')).toBe(false);
21+
expect(utils.isIPv4MappedIPv6Dec('::ffff:4a7d:2b63')).toBe(false);
22+
expect(utils.isIPv4MappedIPv6Dec('::ffff:7f00:800')).toBe(false);
23+
expect(utils.isIPv4MappedIPv6Dec('::ffff:255.255.255.255')).toBe(true);
24+
});
25+
test('detect IPv4 mapped IPv6 Hex addresses', () => {
26+
expect(utils.isIPv4MappedIPv6Hex('::ffff:127.0.0.1')).toBe(false);
27+
expect(utils.isIPv4MappedIPv6Hex('::ffff:7f00:1')).toBe(true);
28+
expect(utils.isIPv4MappedIPv6Hex('::')).toBe(false);
29+
expect(utils.isIPv4MappedIPv6Hex('::1')).toBe(false);
30+
expect(utils.isIPv4MappedIPv6Hex('127.0.0.1')).toBe(false);
31+
expect(utils.isIPv4MappedIPv6Hex('::ffff:4a7d:2b63')).toBe(true);
32+
expect(utils.isIPv4MappedIPv6Hex('::ffff:7f00:800')).toBe(true);
33+
expect(utils.isIPv4MappedIPv6Hex('::ffff:255.255.255.255')).toBe(false);
34+
});
35+
test('to IPv4 mapped IPv6 addresses Dec', () => {
36+
expect(utils.toIPv4MappedIPv6Dec('127.0.0.1')).toBe('::ffff:127.0.0.1');
37+
expect(utils.toIPv4MappedIPv6Dec('0.0.0.0')).toBe('::ffff:0.0.0.0');
38+
expect(utils.toIPv4MappedIPv6Dec('255.255.255.255')).toBe(
1939
'::ffff:255.255.255.255',
2040
);
21-
expect(utils.toIPv4MappedIPv6('74.125.43.99')).toBe('::ffff:74.125.43.99');
41+
expect(utils.toIPv4MappedIPv6Dec('74.125.43.99')).toBe(
42+
'::ffff:74.125.43.99',
43+
);
44+
});
45+
test('to IPv4 mapped IPv6 addresses Hex', () => {
46+
expect(utils.toIPv4MappedIPv6Hex('127.0.0.1')).toBe('::ffff:7f00:1');
47+
expect(utils.toIPv4MappedIPv6Hex('0.0.0.0')).toBe('::ffff:0:0');
48+
expect(utils.toIPv4MappedIPv6Hex('255.255.255.255')).toBe(
49+
'::ffff:ffff:ffff',
50+
);
51+
expect(utils.toIPv4MappedIPv6Hex('74.125.43.99')).toBe('::ffff:4a7d:2b63');
52+
});
53+
test('from IPv4 mapped IPv6 addresses', () => {
2254
expect(utils.fromIPv4MappedIPv6('::ffff:7f00:1')).toBe('127.0.0.1');
2355
expect(utils.fromIPv4MappedIPv6('::ffff:127.0.0.1')).toBe('127.0.0.1');
2456
expect(utils.fromIPv4MappedIPv6('::ffff:0.0.0.0')).toBe('0.0.0.0');
@@ -35,9 +67,33 @@ describe('utils', () => {
3567
expect(utils.fromIPv4MappedIPv6('::ffff:0:0')).toBe('0.0.0.0');
3668
// Converting from ::ffff:7f00:1 to ::ffff:127.0.0.1
3769
expect(
38-
utils.toIPv4MappedIPv6(utils.fromIPv4MappedIPv6('::ffff:7f00:1')),
70+
utils.toIPv4MappedIPv6Dec(utils.fromIPv4MappedIPv6('::ffff:7f00:1')),
3971
).toBe('::ffff:127.0.0.1');
4072
});
73+
test('to canonical IP address', () => {
74+
// IPv4 -> IPv4
75+
expect(utils.toCanonicalIp('127.0.0.1')).toBe('127.0.0.1');
76+
expect(utils.toCanonicalIp('0.0.0.0')).toBe('0.0.0.0');
77+
expect(utils.toCanonicalIp('255.255.255.255')).toBe('255.255.255.255');
78+
expect(utils.toCanonicalIp('74.125.43.99')).toBe('74.125.43.99');
79+
// IPv4 mapped hex -> IPv4
80+
expect(utils.toCanonicalIp('::ffff:7f00:1')).toBe('127.0.0.1');
81+
expect(utils.toCanonicalIp('::ffff:0:0')).toBe('0.0.0.0');
82+
expect(utils.toCanonicalIp('::ffff:ffff:ffff')).toBe('255.255.255.255');
83+
expect(utils.toCanonicalIp('::ffff:4a7d:2b63')).toBe('74.125.43.99');
84+
// IPv4 mapped dec -> IPv4
85+
expect(utils.toCanonicalIp('::ffff:127.0.0.1')).toBe('127.0.0.1');
86+
expect(utils.toCanonicalIp('::ffff:0.0.0.0')).toBe('0.0.0.0');
87+
expect(utils.toCanonicalIp('::ffff:255.255.255.255')).toBe(
88+
'255.255.255.255',
89+
);
90+
expect(utils.toCanonicalIp('::ffff:74.125.43.99')).toBe('74.125.43.99');
91+
// IPv6 -> IPv6
92+
expect(utils.toCanonicalIp('::1234:7f00:1')).toBe('::1234:7f00:1');
93+
expect(utils.toCanonicalIp('::1234:0:0')).toBe('::1234:0:0');
94+
expect(utils.toCanonicalIp('::1234:ffff:ffff')).toBe('::1234:ffff:ffff');
95+
expect(utils.toCanonicalIp('::1234:4a7d:2b63')).toBe('::1234:4a7d:2b63');
96+
});
4197
test('resolves zero IP to local IP', () => {
4298
expect(utils.resolvesZeroIP('0.0.0.0' as Host)).toBe('127.0.0.1');
4399
expect(utils.resolvesZeroIP('::' as Host)).toBe('::1');

0 commit comments

Comments
 (0)