|
| 1 | +import {IPAddress, IPv4, Subnet} from "./index.js"; |
| 2 | + |
| 3 | +/** |
| 4 | + * An IPv6 address |
| 5 | + */ |
| 6 | +export class IPv6 extends IPAddress { |
| 7 | + public static bitLength = 128; |
| 8 | + |
| 9 | + /** |
| 10 | + * Create new IPv6 address instance |
| 11 | + */ |
| 12 | + public constructor(value: bigint) { |
| 13 | + if (value < 0n || value > 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFn) |
| 14 | + throw new TypeError("Expected 128-bit unsigned integer, got " + value.constructor.name + " 0x" + value.toString(16)); |
| 15 | + super(value); |
| 16 | + } |
| 17 | + |
| 18 | + /** |
| 19 | + * Create an IPv6 address instance from hextets |
| 20 | + * @throws {@link !RangeError} If provided hextets are not 8 |
| 21 | + */ |
| 22 | + public static fromBinary(hextets: Uint16Array): IPv6 { |
| 23 | + if (hextets.length !== 8) throw new RangeError("Expected 8 hextets, got " + hextets.length); |
| 24 | + |
| 25 | + return new IPv6( |
| 26 | + BigInt(hextets[0]!) << 112n | |
| 27 | + BigInt(hextets[1]!) << 96n | |
| 28 | + BigInt(hextets[2]!) << 80n | |
| 29 | + BigInt(hextets[3]!) << 64n | |
| 30 | + BigInt(hextets[4]!) << 48n | |
| 31 | + BigInt(hextets[5]!) << 32n | |
| 32 | + BigInt(hextets[6]!) << 16n | |
| 33 | + BigInt(hextets[7]!) |
| 34 | + ); |
| 35 | + } |
| 36 | + |
| 37 | + /** |
| 38 | + * Create an IPv6 address instance from string |
| 39 | + * @throws {@link !RangeError} If provided string is not a valid IPv6 address |
| 40 | + */ |
| 41 | + public static override fromString(str: string): IPv6 { |
| 42 | + const parts = str.split("::", 2); |
| 43 | + const hextestStart = parts[0]! === "" |
| 44 | + ? [] |
| 45 | + : parts[0]!.split(":").flatMap(IPv6.parseHextet); |
| 46 | + const hextestEnd = parts[1] === undefined || parts[1] === "" |
| 47 | + ? [] |
| 48 | + : parts[1].split(":").flatMap(IPv6.parseHextet); |
| 49 | + if ( |
| 50 | + hextestStart.some(hextet => Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF) || |
| 51 | + hextestEnd.some(hextet => Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF) || |
| 52 | + (parts.length === 2 && hextestStart.length + hextestEnd.length > 6) || |
| 53 | + (parts.length < 2 && hextestStart.length + hextestEnd.length !== 8) |
| 54 | + ) throw new RangeError("Expected valid IPv6 address, got " + str); |
| 55 | + |
| 56 | + const hextets = new Uint16Array(8); |
| 57 | + hextets.set(hextestStart, 0); |
| 58 | + hextets.set(hextestEnd, 8 - hextestEnd.length); |
| 59 | + |
| 60 | + return IPv6.fromBinary(hextets); |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * Parse string hextet into unsigned 16-bit integer |
| 65 | + * @internal |
| 66 | + */ |
| 67 | + private static parseHextet(hextet: string): number | number[] { |
| 68 | + return IPv4.regex.test(hextet) |
| 69 | + ? Array.from(new Uint16Array(IPv4.fromString(hextet).binary().buffer)) |
| 70 | + : Number.parseInt(hextet, 16); |
| 71 | + } |
| 72 | + |
| 73 | + /** |
| 74 | + * Get the 8 hextets of the IPv6 address |
| 75 | + */ |
| 76 | + public binary(): Uint16Array { |
| 77 | + return new Uint16Array([ |
| 78 | + (this.value >> 112n) & 0xFFFFn, |
| 79 | + (this.value >> 96n) & 0xFFFFn, |
| 80 | + (this.value >> 80n) & 0xFFFFn, |
| 81 | + (this.value >> 64n) & 0xFFFFn, |
| 82 | + (this.value >> 48n) & 0xFFFFn, |
| 83 | + (this.value >> 32n) & 0xFFFFn, |
| 84 | + (this.value >> 16n) & 0xFFFFn, |
| 85 | + this.value & 0xFFFFn |
| 86 | + ].map(Number)); |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * Check whether this is an IPv4-mapped IPv6 address. |
| 91 | + * Only works for `::ffff:0:0/96` |
| 92 | + */ |
| 93 | + public hasMappedIPv4(): boolean { |
| 94 | + return Subnet.IPV4_MAPPED_IPV6.has(this); |
| 95 | + } |
| 96 | + |
| 97 | + /** |
| 98 | + * Get the IPv4-mapped IPv6 address. |
| 99 | + * Returns the last 32 bits as an IPv4 address. |
| 100 | + * @see {@link IPv6#hasMappedIPv4} |
| 101 | + */ |
| 102 | + public getMappedIPv4(): IPv4 { |
| 103 | + return IPv4.fromBinary(new Uint8Array(this.binary().buffer).slice(-4)); |
| 104 | + } |
| 105 | + |
| 106 | + public override toString(): string { |
| 107 | + const str = Array.from(this.binary()).map(octet => octet.toString(16)).join(":"); |
| 108 | + const longest = str.match(/(?:^|:)0(?::0)+(?:$|:)/g)?.reduce((acc, cur) => cur.length > acc.length ? cur : acc, "") ?? null; |
| 109 | + if (longest === null) return str; |
| 110 | + return str.replace(longest, "::"); |
| 111 | + } |
| 112 | +} |
0 commit comments