1 | export const max4 = 2n ** 32n - 1n;
|
2 | export const max6 = 2n ** 128n - 1n;
|
3 | const isIP = ip => ip.includes(":") ? 6 : ip.includes(".") ? 4 : 0;
|
4 |
|
5 | export function parseIp(ip) {
|
6 | const version = isIP(ip);
|
7 | if (!version) throw new Error(`Invalid IP address: ${ip}`);
|
8 |
|
9 | const result = Object.create(null);
|
10 | let number = 0n;
|
11 | let exp = 0n;
|
12 |
|
13 | if (version === 4) {
|
14 | for (const n of ip.split(".").map(BigInt).reverse()) {
|
15 | number += n * (2n ** exp);
|
16 | exp += 8n;
|
17 | }
|
18 |
|
19 | result.number = number;
|
20 | result.version = version;
|
21 | return result;
|
22 | } else {
|
23 | if (ip.includes(".")) {
|
24 | result.ipv4mapped = true;
|
25 | ip = ip.split(":").map(part => {
|
26 | if (part.includes(".")) {
|
27 | const digits = part.split(".").map(str => Number(str).toString(16).padStart(2, "0"));
|
28 | return `${digits[0]}${digits[1]}:${digits[2]}${digits[3]}`;
|
29 | } else {
|
30 | return part;
|
31 | }
|
32 | }).join(":");
|
33 | }
|
34 |
|
35 | if (ip.includes("%")) {
|
36 | let scopeid;
|
37 | [, ip, scopeid] = /(.+)%(.+)/.exec(ip);
|
38 | result.scopeid = scopeid;
|
39 | }
|
40 |
|
41 | const parts = ip.split(":");
|
42 | const index = parts.indexOf("");
|
43 |
|
44 | if (index !== -1) {
|
45 | while (parts.length < 8) {
|
46 | parts.splice(index, 0, "");
|
47 | }
|
48 | }
|
49 |
|
50 | for (const n of parts.map(part => BigInt(parseInt(part || 0, 16))).reverse()) {
|
51 | number += n * (2n ** exp);
|
52 | exp += 16n;
|
53 | }
|
54 |
|
55 | result.number = number;
|
56 | result.version = version;
|
57 | return result;
|
58 | }
|
59 | }
|
60 |
|
61 | export function stringifyIp({number, version, ipv4mapped, scopeid} = {}) {
|
62 | if (typeof number !== "bigint") throw new Error(`Expected a BigInt`);
|
63 | if (![4, 6].includes(version)) throw new Error(`Invalid version: ${version}`);
|
64 | if (number < 0n || number > (version === 4 ? max4 : max6)) throw new Error(`Invalid number: ${number}`);
|
65 |
|
66 | let step = version === 4 ? 24n : 112n;
|
67 | const stepReduction = version === 4 ? 8n : 16n;
|
68 | let remain = number;
|
69 | const parts = [];
|
70 |
|
71 | while (step > 0n) {
|
72 | const divisor = 2n ** step;
|
73 | parts.push(remain / divisor);
|
74 | remain = number % divisor;
|
75 | step -= stepReduction;
|
76 | }
|
77 | parts.push(remain);
|
78 |
|
79 | if (version === 4) {
|
80 | return parts.join(".");
|
81 | } else {
|
82 | let ip = "";
|
83 | if (ipv4mapped) {
|
84 | for (const [index, num] of parts.entries()) {
|
85 | if (index < 6) {
|
86 | ip += `${num.toString(16)}:`;
|
87 | } else {
|
88 | ip += `${String(num >> 8n)}.${String(num & 255n)}${index === 6 ? "." : ""}`;
|
89 | }
|
90 | }
|
91 | ip = compressIPv6(ip.split(":"));
|
92 | } else {
|
93 | ip = compressIPv6(parts.map(n => n.toString(16)));
|
94 | }
|
95 |
|
96 | if (scopeid) {
|
97 | ip = `${ip}%${scopeid}`;
|
98 | }
|
99 |
|
100 | return ip;
|
101 | }
|
102 | }
|
103 |
|
104 | export function normalizeIp(ip) {
|
105 | return stringifyIp(parseIp(ip));
|
106 | }
|
107 |
|
108 |
|
109 | function compressIPv6(parts) {
|
110 | let longestSequence;
|
111 | let currentSequence;
|
112 | for (const [index, part] of parts.entries()) {
|
113 | if (part === "0") {
|
114 | if (!currentSequence) {
|
115 | currentSequence = new Set([index]);
|
116 | } else {
|
117 | currentSequence.add(index);
|
118 | }
|
119 | } else {
|
120 | if (currentSequence) {
|
121 | if (!longestSequence) {
|
122 | longestSequence = currentSequence;
|
123 | } else if (currentSequence.size > longestSequence.size) {
|
124 | longestSequence = currentSequence;
|
125 | }
|
126 | currentSequence = null;
|
127 | }
|
128 | }
|
129 | }
|
130 | if (!longestSequence && currentSequence) longestSequence = currentSequence;
|
131 |
|
132 | for (const index of longestSequence || []) {
|
133 | parts[index] = ":";
|
134 | }
|
135 |
|
136 | return parts.filter(Boolean).join(":").replace(/:{2,}/, "::");
|
137 | }
|