diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 264c7a4..f4c4e36 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -94,6 +94,9 @@ jobs:
- name: Check types
run: deno check src tests --sloppy-imports
+ - name: Check formatting
+ run: deno fmt --check
+
- name: Test
run: deno task test
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 4e7094c..4461be1 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -11,7 +11,7 @@ on:
jobs:
analyze:
name: Analyse (${{ matrix.language }})
- runs-on: 'ubuntu-latest'
+ runs-on: "ubuntu-latest"
timeout-minutes: 360
permissions:
security-events: write
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 8ca546d..38d1d55 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -3,4 +3,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/copyright/Copyright_Cloudnode___LGPL_3_0.xml b/.idea/copyright/Copyright_Cloudnode___LGPL_3_0.xml
index 2004c3c..78c898e 100644
--- a/.idea/copyright/Copyright_Cloudnode___LGPL_3_0.xml
+++ b/.idea/copyright/Copyright_Cloudnode___LGPL_3_0.xml
@@ -1,6 +1,9 @@
-
+
-
\ No newline at end of file
+
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
index f37a35c..66a4eb1 100644
--- a/.idea/copyright/profiles_settings.xml
+++ b/.idea/copyright/profiles_settings.xml
@@ -4,4 +4,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index d903933..0017d2c 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -4,4 +4,4 @@
ffff
-
\ No newline at end of file
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index f8e1373..4fc0c37 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,10 @@
-
+
-
\ No newline at end of file
+
diff --git a/.idea/runConfigurations/build.xml b/.idea/runConfigurations/build.xml
index 0a4f98f..dce8a2a 100644
--- a/.idea/runConfigurations/build.xml
+++ b/.idea/runConfigurations/build.xml
@@ -1,5 +1,10 @@
-
+
@@ -9,4 +14,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/runConfigurations/test.xml b/.idea/runConfigurations/test.xml
index b46fef0..41b0eea 100644
--- a/.idea/runConfigurations/test.xml
+++ b/.idea/runConfigurations/test.xml
@@ -1,5 +1,10 @@
-
+
@@ -9,4 +14,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..5ace414 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -3,4 +3,4 @@
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index bd63d22..01e528e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,8 @@
[](https://github.com/cloudnode-pro/ip/actions/workflows/ci.yml)

-A modern, object-oriented TypeScript library for representing and performing arithmetic on IP addresses and subnets.
+A modern, object-oriented TypeScript library for representing and performing
+arithmetic on IP addresses and subnets.
[**Documentation — API Reference**](https://ip.cldn.pro)
@@ -25,7 +26,7 @@ npm install @cldn/ip
Import and use:
```ts
-import {IPv4, IPv6, Subnet} from "@cldn/ip";
+import { IPv4, IPv6, Subnet } from "@cldn/ip";
```
### Deno
@@ -33,39 +34,41 @@ import {IPv4, IPv6, Subnet} from "@cldn/ip";
Import the package from npm using the standard prefix:
```ts
-import {IPv4, IPv6, Subnet} from "npm:@cldn/ip";
+import { IPv4, IPv6, Subnet } from "npm:@cldn/ip";
```
### Browsers
-For browser usage, it is recommended to use a bundler like [Vite](https://vitejs.dev/),
-or [Webpack](https://webpack.js.org/). If you are using a bundler, follow the same usage as for Node.js.
+For browser usage, it is recommended to use a bundler like
+[Vite](https://vitejs.dev/), or [Webpack](https://webpack.js.org/). If you are
+using a bundler, follow the same usage as for Node.js.
-Alternatively, you can import the library as
-a [JavaScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
+Alternatively, you can import the library as a
+[JavaScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
from [ESM>CDN](https://esm.sh/):
```html
-
```
## Features
- Object-oriented representation of IPv4 and IPv6 addresses, and subnets.
-- Comprehensive subnet arithmetic operations (e.g., containment, splitting, merging).
+- Comprehensive subnet arithmetic operations (e.g., containment, splitting,
+ merging).
- Support for CIDR notation for defining and parsing subnets.
- Easy definition and manipulation of networks and collections of subnets.
- Support for IPv4-mapped IPv6 addresses.
- Fully documented, fully typed, and thoroughly tested with 100% coverage.
-- Zero dependencies; compatible with frontend and backend environments without requiring polyfills.
+- Zero dependencies; compatible with frontend and backend environments without
+ requiring polyfills.
## Example
```ts
-import {IPv4, Subnet} from "@cldn/ip";
+import { IPv4, Subnet } from "@cldn/ip";
// Parse IPv4 address
const ip = IPv4.fromString("213.0.113.42");
@@ -80,13 +83,15 @@ console.log(subnet.contains(ip)); // true
## Contact
-For bugs, or feature requests, please use [GitHub Issues](https://github.com/cloudnode-pro/ip/issues).
+For bugs, or feature requests, please use
+[GitHub Issues](https://github.com/cloudnode-pro/ip/issues).
-For real-time chat or community discussions, join our Matrix
-space: [#community\:cloudnode.pro](https://matrix.to/#/%23community:cloudnode.pro).
+For real-time chat or community discussions, join our Matrix space:
+[#community\:cloudnode.pro](https://matrix.to/#/%23community:cloudnode.pro).
## Licence
Copyright © 2024–2025 Cloudnode OÜ.
-This project is licensed under the terms of the [LGPL-3.0](https://github.com/cloudnode-pro/ip/blob/master/COPYING) licence.
+This project is licensed under the terms of the
+[LGPL-3.0](https://github.com/cloudnode-pro/ip/blob/master/COPYING) licence.
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 46b2be8..074854b 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,37 +1,38 @@
-import {defineConfig} from "vitepress";
+import { defineConfig } from "vitepress";
import typedocSidebar from "../api/typedoc-sidebar.json";
// https://vitepress.dev/reference/site-config
export default defineConfig({
- title: "@cldn/ip",
- description: "Documentation",
- cleanUrls: true,
- themeConfig: {
- nav: [
- {text: "API Reference", link: "/api/"},
- ],
+ title: "@cldn/ip",
+ description: "Documentation",
+ cleanUrls: true,
+ themeConfig: {
+ nav: [
+ { text: "API Reference", link: "/api/" },
+ ],
- sidebar: [
- {
- text: "API",
- items: typedocSidebar,
- },
- ],
+ sidebar: [
+ {
+ text: "API",
+ items: typedocSidebar,
+ },
+ ],
- outline: [2, 3],
+ outline: [2, 3],
- socialLinks: [
- {icon: "github", link: "https://github.com/cloudnode-pro/ip"},
- {icon: "matrix", link: "https://matrix.to/#/@cloudnode:matrix.org"},
- ],
+ socialLinks: [
+ { icon: "github", link: "https://github.com/cloudnode-pro/ip" },
+ { icon: "matrix", link: "https://matrix.to/#/@cloudnode:matrix.org" },
+ ],
- search: {
- provider: "local",
- },
+ search: {
+ provider: "local",
+ },
- footer: {
- copyright: `Copyright © 2024–2025 Cloudnode OÜ.`,
- message: `Released under the LGPL-3.0 licence.`
- }
+ footer: {
+ copyright: `Copyright © 2024–2025 Cloudnode OÜ.`,
+ message:
+ `Released under the LGPL-3.0 licence.`,
},
+ },
});
diff --git a/src/IP.ts b/src/IP.ts
index 910b8c3..d0f7b6f 100644
--- a/src/IP.ts
+++ b/src/IP.ts
@@ -14,8 +14,8 @@
* You should have received a copy of the GNU Lesser General Public License along with @cldn/ip.
* If not, see
*/
-import {IPv4} from "./IPv4.js";
-import {IPv6} from "./IPv6.js";
+import { IPv4 } from "./IPv4.js";
+import { IPv6 } from "./IPv6.js";
/**
* An {@link IPv4} or {@link IPv6} address.
@@ -23,8 +23,8 @@ import {IPv6} from "./IPv6.js";
export type IP = IPv4 | IPv6;
export namespace IP {
- /**
- * An {@link IPv4} or {@link IPv6} address class.
- */
- export type Class = typeof IPv4 | typeof IPv6;
+ /**
+ * An {@link IPv4} or {@link IPv6} address class.
+ */
+ export type Class = typeof IPv4 | typeof IPv6;
}
diff --git a/src/IPAddress.ts b/src/IPAddress.ts
index 1d546c6..8cfe344 100644
--- a/src/IPAddress.ts
+++ b/src/IPAddress.ts
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Lesser General Public License along with @cldn/ip.
* If not, see .
*/
-import {IP, IPv4, IPv6} from "./index.js";
+import { IP, IPv4, IPv6 } from "./index.js";
/**
* Represents an Internet Protocol (IP) address.
@@ -22,77 +22,81 @@ import {IP, IPv4, IPv6} from "./index.js";
* @sealed
*/
export abstract class IPAddress {
- /**
- * Integer representation of the IP address.
- */
- public readonly value: bigint;
+ /**
+ * Integer representation of the IP address.
+ */
+ public readonly value: bigint;
- /**
- * Creates a new IP address instance.
- *
- * @param value Integer representation of the IP address.
- * @internal
- */
- protected constructor(value: bigint) {
- this.value = value;
- }
+ /**
+ * Creates a new IP address instance.
+ *
+ * @param value Integer representation of the IP address.
+ * @internal
+ */
+ protected constructor(value: bigint) {
+ this.value = value;
+ }
- /**
- * Creates an IP address from a string.
- *
- * @param ip String representation of the IP address.
- * @param [resolveMapped=false] Whether to resolve IPv4-mapped IPv6 addresses (see {@link IPv6.hasMappedIPv4}).
- * @throws {@link !RangeError} If the string is not a valid IPv4 or IPv6 address.
- */
- public static fromString(ip: string, resolveMapped = false): IP {
- if (ip.includes(":")) {
- const ipv6 = IPv6.fromString(ip);
- if (resolveMapped && ipv6.hasMappedIPv4())
- return ipv6.getMappedIPv4();
- return ipv6;
- }
- return IPv4.fromString(ip);
+ /**
+ * Creates an IP address from a string.
+ *
+ * @param ip String representation of the IP address.
+ * @param [resolveMapped=false] Whether to resolve IPv4-mapped IPv6 addresses (see {@link IPv6.hasMappedIPv4}).
+ * @throws {@link !RangeError} If the string is not a valid IPv4 or IPv6 address.
+ */
+ public static fromString(ip: string, resolveMapped = false): IP {
+ if (ip.includes(":")) {
+ const ipv6 = IPv6.fromString(ip);
+ if (resolveMapped && ipv6.hasMappedIPv4()) {
+ return ipv6.getMappedIPv4();
+ }
+ return ipv6;
}
+ return IPv4.fromString(ip);
+ }
- /**
- * Returns the binary representation of the IP address.
- */
- public abstract binary(): ArrayBufferView;
+ /**
+ * Returns the binary representation of the IP address.
+ */
+ public abstract binary(): ArrayBufferView;
- /**
- * Checks if the given address is equal to this address.
- *
- * @param other Address to compare.
- */
- public equals(other: IPAddress): boolean {
- return other instanceof this.constructor && other.value === this.value;
- }
+ /**
+ * Checks if the given address is equal to this address.
+ *
+ * @param other Address to compare.
+ */
+ public equals(other: IPAddress): boolean {
+ return other instanceof this.constructor && other.value === this.value;
+ }
- /**
- * Returns the IP address as a bigint or string primitive.
- *
- * @param hint Preferred primitive type.
- */
- public [Symbol.toPrimitive](hint: "number" | "string" | "default"): bigint | string {
- if (hint === "string")
- return this.toString();
- return this.value;
+ /**
+ * Returns the IP address as a bigint or string primitive.
+ *
+ * @param hint Preferred primitive type.
+ */
+ public [Symbol.toPrimitive](
+ hint: "number" | "string" | "default",
+ ): bigint | string {
+ if (hint === "string") {
+ return this.toString();
}
+ return this.value;
+ }
- /**
- * Returns the IP address as a string.
- */
- public abstract toString(): string;
+ /**
+ * Returns the IP address as a string.
+ */
+ public abstract toString(): string;
- /**
- * Returns a new IP address offset by the given amount from this address.
- *
- * @example ip.offset(1) // Returns the next IP address.
- * @example ip.offset(-1) // Returns the previous IP address.
- * @example IPAddress.fromString("203.0.113.42").offset(-18) // Returns 203.0.113.24.
- *
- * @param offset Number of steps to offset, positive or negative.
- * @throws {@link !TypeError} If the resulting address is outside the IP address family range.
- */
- public abstract offset(offset: number | bigint): IPAddress;
+ /**
+ * Returns a new IP address offset by the given amount from this address.
+ *
+ * @example ip.offset(1) // Returns the next IP address.
+ * @example ip.offset(-1) // Returns the previous IP address.
+ * @example IPAddress.fromString("203.0.113.42").offset(-18) // Returns 203.0.113.24.
+ *
+ * @param offset Number of steps to offset, positive or negative.
+ * @throws {@link !TypeError} If the resulting address is outside the IP address family range.
+ */
+ public abstract offset(offset: number | bigint): IPAddress;
}
diff --git a/src/IPv4.ts b/src/IPv4.ts
index 7666b2f..be82cd8 100644
--- a/src/IPv4.ts
+++ b/src/IPv4.ts
@@ -14,98 +14,110 @@
* You should have received a copy of the GNU Lesser General Public License along with @cldn/ip.
* If not, see .
*/
-import {IPAddress} from "./index.js";
+import { IPAddress } from "./index.js";
/**
* Represents an Internet Protocol version 4 (IPv4) address.
*/
export class IPv4 extends IPAddress {
- /**
- * Regular expression for testing IPv4 addresses in dotted-decimal string notation.
- */
- public static REGEX = /^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/;
+ /**
+ * Regular expression for testing IPv4 addresses in dotted-decimal string notation.
+ */
+ public static REGEX =
+ /^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/;
- /**
- * Bit length of IPv4 addresses.
- */
- public static BIT_LENGTH = 32;
+ /**
+ * Bit length of IPv4 addresses.
+ */
+ public static BIT_LENGTH = 32;
- /**
- * Creates a new IPv4 address instance.
- *
- * @param value 32-bit unsigned integer.
- * @throws {@link !TypeError} If the value is not a 32-bit unsigned integer.
- */
- public constructor(value: bigint);
+ /**
+ * Creates a new IPv4 address instance.
+ *
+ * @param value 32-bit unsigned integer.
+ * @throws {@link !TypeError} If the value is not a 32-bit unsigned integer.
+ */
+ public constructor(value: bigint);
- /**
- * Creates a new IPv4 address instance.
- *
- * @param value 32-bit unsigned number.
- * @throws {@link !TypeError} If the value is not a 32-bit unsigned integer.
- */
- public constructor(value: number);
+ /**
+ * Creates a new IPv4 address instance.
+ *
+ * @param value 32-bit unsigned number.
+ * @throws {@link !TypeError} If the value is not a 32-bit unsigned integer.
+ */
+ public constructor(value: number);
- public constructor(value: number | bigint) {
- const int = BigInt(value);
- if (int < 0n || int > (1n << BigInt(IPv4.BIT_LENGTH)) - 1n)
- throw new TypeError("Expected 32-bit unsigned integer, got " + int.constructor.name + " " + int.toString(10));
- super(int);
+ public constructor(value: number | bigint) {
+ const int = BigInt(value);
+ if (int < 0n || int > (1n << BigInt(IPv4.BIT_LENGTH)) - 1n) {
+ throw new TypeError(
+ "Expected 32-bit unsigned integer, got " + int.constructor.name + " " +
+ int.toString(10),
+ );
}
+ super(int);
+ }
- /**
- * Creates an IPv4 address instance from octets.
- *
- * @param octets Typed array of 4 octets.
- * @throws {@link !RangeError} If the number of octets is not 4.
- */
- public static fromBinary(octets: Uint8Array): IPv4 {
- if (octets.length !== 4) throw new RangeError("Expected 4 octets, got " + octets.length);
-
- return new IPv4(
- (
- octets[0]! << 24 |
- octets[1]! << 16 |
- octets[2]! << 8 |
- octets[3]!
- ) >>> 0
- );
+ /**
+ * Creates an IPv4 address instance from octets.
+ *
+ * @param octets Typed array of 4 octets.
+ * @throws {@link !RangeError} If the number of octets is not 4.
+ */
+ public static fromBinary(octets: Uint8Array): IPv4 {
+ if (octets.length !== 4) {
+ throw new RangeError("Expected 4 octets, got " + octets.length);
}
- /**
- * Creates an IPv4 address instance from a string.
- *
- * @param ip String representation of an IPv4 address.
- * @throws {@link !RangeError} If the string is not a valid IPv4 address.
- */
- public static override fromString(ip: string): IPv4 {
- const octets = ip.split(".", 4).map(octet => Number.parseInt(octet, 10));
- if (octets.some(octet => Number.isNaN(octet) || octet < 0 || octet > 255))
- throw new RangeError("Expected valid IPv4 address, got " + ip);
+ return new IPv4(
+ (
+ octets[0]! << 24 |
+ octets[1]! << 16 |
+ octets[2]! << 8 |
+ octets[3]!
+ ) >>> 0,
+ );
+ }
- return IPv4.fromBinary(new Uint8Array(octets));
+ /**
+ * Creates an IPv4 address instance from a string.
+ *
+ * @param ip String representation of an IPv4 address.
+ * @throws {@link !RangeError} If the string is not a valid IPv4 address.
+ */
+ public static override fromString(ip: string): IPv4 {
+ const octets = ip.split(".", 4).map((octet) => Number.parseInt(octet, 10));
+ if (
+ octets.some((octet) => Number.isNaN(octet) || octet < 0 || octet > 255)
+ ) {
+ throw new RangeError("Expected valid IPv4 address, got " + ip);
}
- /**
- * Returns the 4 octets of the IPv4 address.
- */
- public override binary(): Uint8Array {
- return new Uint8Array([
- (this.value >> 24n) & 0xFFn,
- (this.value >> 16n) & 0xFFn,
- (this.value >> 8n) & 0xFFn,
- this.value & 0xFFn,
- ].map(Number));
- }
+ return IPv4.fromBinary(new Uint8Array(octets));
+ }
- /**
- * Returns the IP address as a string in dotted-decimal notation.
- */
- public override toString(): string {
- return Array.from(this.binary()).map(octet => octet.toString(10)).join(".");
- }
+ /**
+ * Returns the 4 octets of the IPv4 address.
+ */
+ public override binary(): Uint8Array {
+ return new Uint8Array([
+ (this.value >> 24n) & 0xFFn,
+ (this.value >> 16n) & 0xFFn,
+ (this.value >> 8n) & 0xFFn,
+ this.value & 0xFFn,
+ ].map(Number));
+ }
- public override offset(offset: bigint | number): IPv4 {
- return new IPv4(this.value + BigInt(offset));
- }
+ /**
+ * Returns the IP address as a string in dotted-decimal notation.
+ */
+ public override toString(): string {
+ return Array.from(this.binary()).map((octet) => octet.toString(10)).join(
+ ".",
+ );
+ }
+
+ public override offset(offset: bigint | number): IPv4 {
+ return new IPv4(this.value + BigInt(offset));
+ }
}
diff --git a/src/IPv6.ts b/src/IPv6.ts
index 8d7ef13..101283e 100644
--- a/src/IPv6.ts
+++ b/src/IPv6.ts
@@ -14,147 +14,160 @@
* You should have received a copy of the GNU Lesser General Public License along with @cldn/ip.
* If not, see .
*/
-import {IPAddress, IPv4, Subnet} from "./index.js";
+import { IPAddress, IPv4, Subnet } from "./index.js";
/**
* Represents an Internet Protocol version 6 (IPv6) address.
*/
export class IPv6 extends IPAddress {
- /**
- * Bit length of IPv6 addresses.
- */
- public static BIT_LENGTH = 128;
+ /**
+ * Bit length of IPv6 addresses.
+ */
+ public static BIT_LENGTH = 128;
- /**
- * Creates a new IPv6 address instance.
- *
- * @param value 128-bit unsigned integer.
- * @throws {@link !TypeError} If the value is not a 128-bit unsigned integer.
- */
- public constructor(value: bigint) {
- if (value < 0n || value > ((1n << BigInt(IPv6.BIT_LENGTH)) - 1n))
- throw new TypeError("Expected 128-bit unsigned integer, got " + value.constructor.name + " 0x" + value.toString(16));
- super(value);
+ /**
+ * Creates a new IPv6 address instance.
+ *
+ * @param value 128-bit unsigned integer.
+ * @throws {@link !TypeError} If the value is not a 128-bit unsigned integer.
+ */
+ public constructor(value: bigint) {
+ if (value < 0n || value > ((1n << BigInt(IPv6.BIT_LENGTH)) - 1n)) {
+ throw new TypeError(
+ "Expected 128-bit unsigned integer, got " + value.constructor.name +
+ " 0x" + value.toString(16),
+ );
}
+ super(value);
+ }
- /**
- * Creates an IPv6 address instance from hextets.
- *
- * @param hextets Typed array of 8 hextets.
- * @throws {@link !RangeError} If the number of hextets is not 8.
- */
- public static fromBinary(hextets: Uint16Array): IPv6 {
- if (hextets.length !== 8) throw new RangeError("Expected 8 hextets, got " + hextets.length);
-
- return new IPv6(
- BigInt(hextets[0]!) << 112n |
- BigInt(hextets[1]!) << 96n |
- BigInt(hextets[2]!) << 80n |
- BigInt(hextets[3]!) << 64n |
- BigInt(hextets[4]!) << 48n |
- BigInt(hextets[5]!) << 32n |
- BigInt(hextets[6]!) << 16n |
- BigInt(hextets[7]!)
- );
+ /**
+ * Creates an IPv6 address instance from hextets.
+ *
+ * @param hextets Typed array of 8 hextets.
+ * @throws {@link !RangeError} If the number of hextets is not 8.
+ */
+ public static fromBinary(hextets: Uint16Array): IPv6 {
+ if (hextets.length !== 8) {
+ throw new RangeError("Expected 8 hextets, got " + hextets.length);
}
- /**
- * Creates an IPv6 address instance from a string.
- *
- * @param ip String representation of an IPv6 address.
- * @throws {@link !RangeError} If the string is not a valid IPv6 address.
- */
- public static override fromString(ip: string): IPv6 {
- const parts = ip.split("::", 2);
- const hextestStart = parts[0]! === ""
- ? []
- : parts[0]!.split(":").flatMap(IPv6.parseHextet);
- const hextestEnd = parts[1] === undefined || parts[1] === ""
- ? []
- : parts[1].split(":").flatMap(IPv6.parseHextet);
- if (
- hextestStart.some(hextet => Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF) ||
- hextestEnd.some(hextet => Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF) ||
- (parts.length === 2 && hextestStart.length + hextestEnd.length > 6) ||
- (parts.length < 2 && hextestStart.length + hextestEnd.length !== 8)
- ) throw new RangeError("Expected valid IPv6 address, got " + ip);
+ return new IPv6(
+ BigInt(hextets[0]!) << 112n |
+ BigInt(hextets[1]!) << 96n |
+ BigInt(hextets[2]!) << 80n |
+ BigInt(hextets[3]!) << 64n |
+ BigInt(hextets[4]!) << 48n |
+ BigInt(hextets[5]!) << 32n |
+ BigInt(hextets[6]!) << 16n |
+ BigInt(hextets[7]!),
+ );
+ }
- const hextets = new Uint16Array(8);
- hextets.set(hextestStart, 0);
- hextets.set(hextestEnd, 8 - hextestEnd.length);
+ /**
+ * Creates an IPv6 address instance from a string.
+ *
+ * @param ip String representation of an IPv6 address.
+ * @throws {@link !RangeError} If the string is not a valid IPv6 address.
+ */
+ public static override fromString(ip: string): IPv6 {
+ const parts = ip.split("::", 2);
+ const hextestStart = parts[0]! === ""
+ ? []
+ : parts[0]!.split(":").flatMap(IPv6.parseHextet);
+ const hextestEnd = parts[1] === undefined || parts[1] === ""
+ ? []
+ : parts[1].split(":").flatMap(IPv6.parseHextet);
+ if (
+ hextestStart.some((hextet) =>
+ Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF
+ ) ||
+ hextestEnd.some((hextet) =>
+ Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF
+ ) ||
+ (parts.length === 2 && hextestStart.length + hextestEnd.length > 6) ||
+ (parts.length < 2 && hextestStart.length + hextestEnd.length !== 8)
+ ) throw new RangeError("Expected valid IPv6 address, got " + ip);
- return IPv6.fromBinary(hextets);
- }
+ const hextets = new Uint16Array(8);
+ hextets.set(hextestStart, 0);
+ hextets.set(hextestEnd, 8 - hextestEnd.length);
- /**
- * Parses a string hextet into unsigned 16-bit integer.
- *
- * @param hextet String representation of a hextet.
- * @internal
- */
- private static parseHextet(hextet: string): number | number[] {
- if (IPv4.REGEX.test(hextet)) {
- const ip = IPv4.fromString(hextet).binary();
- return [
- (ip[0]! << 8) | ip[1]!,
- (ip[2]! << 8) | ip[3]!
- ]
- }
- return Number.parseInt(hextet, 16);
- }
+ return IPv6.fromBinary(hextets);
+ }
- /**
- * Returns the 8 hextets of the IPv6 address.
- */
- public binary(): Uint16Array {
- return new Uint16Array([
- (this.value >> 112n) & 0xFFFFn,
- (this.value >> 96n) & 0xFFFFn,
- (this.value >> 80n) & 0xFFFFn,
- (this.value >> 64n) & 0xFFFFn,
- (this.value >> 48n) & 0xFFFFn,
- (this.value >> 32n) & 0xFFFFn,
- (this.value >> 16n) & 0xFFFFn,
- this.value & 0xFFFFn
- ].map(Number));
+ /**
+ * Parses a string hextet into unsigned 16-bit integer.
+ *
+ * @param hextet String representation of a hextet.
+ * @internal
+ */
+ private static parseHextet(hextet: string): number | number[] {
+ if (IPv4.REGEX.test(hextet)) {
+ const ip = IPv4.fromString(hextet).binary();
+ return [
+ (ip[0]! << 8) | ip[1]!,
+ (ip[2]! << 8) | ip[3]!,
+ ];
}
+ return Number.parseInt(hextet, 16);
+ }
- /**
- * Checks whether this IPv6 address is an IPv4-mapped IPv6 address as defined by the `::ffff:0:0/96` prefix. This
- * method does not detect other IPv4 embedding or tunnelling formats.
- */
- public hasMappedIPv4(): boolean {
- return Subnet.IPV4_MAPPED_IPV6.contains(this);
- }
+ /**
+ * Returns the 8 hextets of the IPv6 address.
+ */
+ public binary(): Uint16Array {
+ return new Uint16Array([
+ (this.value >> 112n) & 0xFFFFn,
+ (this.value >> 96n) & 0xFFFFn,
+ (this.value >> 80n) & 0xFFFFn,
+ (this.value >> 64n) & 0xFFFFn,
+ (this.value >> 48n) & 0xFFFFn,
+ (this.value >> 32n) & 0xFFFFn,
+ (this.value >> 16n) & 0xFFFFn,
+ this.value & 0xFFFFn,
+ ].map(Number));
+ }
- /**
- * Returns the IPv4-mapped IPv6 address.
- *
- * @returns The IPv4 address from the least significant 32 bits of this IPv6 address.
- * @see {@link IPv6#hasMappedIPv4}
- */
- public getMappedIPv4(): IPv4 {
- const bin = this.binary().slice(-2);
- return IPv4.fromBinary(new Uint8Array([
- (bin[0]! >> 8) & 0xFF,
- bin[0]! & 0xFF,
- (bin[1]! >> 8) & 0xFF,
- bin[1]! & 0xFF
- ]));
- }
+ /**
+ * Checks whether this IPv6 address is an IPv4-mapped IPv6 address as defined by the `::ffff:0:0/96` prefix. This
+ * method does not detect other IPv4 embedding or tunnelling formats.
+ */
+ public hasMappedIPv4(): boolean {
+ return Subnet.IPV4_MAPPED_IPV6.contains(this);
+ }
- /**
- * Returns the IP address as a string in colon-hexadecimal notation.
- */
- public override toString(): string {
- const str = Array.from(this.binary()).map(octet => octet.toString(16)).join(":");
- const longest = str.match(/(?:^|:)0(?::0)+(?:$|:)/g)
- ?.reduce((acc, cur) => cur.length > acc.length ? cur : acc, "") ?? null;
- if (longest === null) return str;
- return str.replace(longest, "::");
- }
+ /**
+ * Returns the IPv4-mapped IPv6 address.
+ *
+ * @returns The IPv4 address from the least significant 32 bits of this IPv6 address.
+ * @see {@link IPv6#hasMappedIPv4}
+ */
+ public getMappedIPv4(): IPv4 {
+ const bin = this.binary().slice(-2);
+ return IPv4.fromBinary(
+ new Uint8Array([
+ (bin[0]! >> 8) & 0xFF,
+ bin[0]! & 0xFF,
+ (bin[1]! >> 8) & 0xFF,
+ bin[1]! & 0xFF,
+ ]),
+ );
+ }
- public override offset(offset: bigint | number): IPv6 {
- return new IPv6(this.value + BigInt(offset));
- }
+ /**
+ * Returns the IP address as a string in colon-hexadecimal notation.
+ */
+ public override toString(): string {
+ const str = Array.from(this.binary()).map((octet) => octet.toString(16))
+ .join(":");
+ const longest = str.match(/(?:^|:)0(?::0)+(?:$|:)/g)
+ ?.reduce((acc, cur) => cur.length > acc.length ? cur : acc, "") ?? null;
+ if (longest === null) return str;
+ return str.replace(longest, "::");
+ }
+
+ public override offset(offset: bigint | number): IPv6 {
+ return new IPv6(this.value + BigInt(offset));
+ }
}
diff --git a/src/Network.ts b/src/Network.ts
index c9397be..c586c38 100644
--- a/src/Network.ts
+++ b/src/Network.ts
@@ -14,26 +14,26 @@
* You should have received a copy of the GNU Lesser General Public License along with @cldn/ip.
* If not, see
*/
-import {IP} from "./IP.js";
+import { IP } from "./IP.js";
/**
* A network of IP addresses.
*/
export interface Network extends Iterable {
- /**
- * Determines whether this network contains the provided IP address.
- *
- * @param address IP address to check.
- */
- contains(address: T): boolean;
+ /**
+ * Determines whether this network contains the provided IP address.
+ *
+ * @param address IP address to check.
+ */
+ contains(address: T): boolean;
- /**
- * Returns the exact number of IP addresses in this network.
- */
- size(): bigint;
+ /**
+ * Returns the exact number of IP addresses in this network.
+ */
+ size(): bigint;
- /**
- * Iterates over all IP addresses in this network.
- */
- [Symbol.iterator](): Iterator;
+ /**
+ * Iterates over all IP addresses in this network.
+ */
+ [Symbol.iterator](): Iterator;
}
diff --git a/src/Subnet.ts b/src/Subnet.ts
index b8da753..5b9527d 100644
--- a/src/Subnet.ts
+++ b/src/Subnet.ts
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Lesser General Public License along with @cldn/ip.
* If not, see .
*/
-import {IP, IPAddress, Network} from "./index.js";
+import { IP, IPAddress, Network } from "./index.js";
/**
* Represents an address range subnetwork of Internet Protocol (IP) addresses.
@@ -22,315 +22,355 @@ import {IP, IPAddress, Network} from "./index.js";
* @typeParam T IP address family.
*/
export class Subnet implements Network {
- /**
- * Subnet for IPv4-mapped IPv6 addresses (`::ffff:0.0.0.0/96`).
- */
- public static readonly IPV4_MAPPED_IPV6 = Subnet.fromCIDR("::ffff:0.0.0.0/96");
-
- /**
- * Subnet address (i.e. the first IP address of the network).
- */
- public readonly address: T;
-
- /**
- * Bit length of the subnet prefix.
- */
- public readonly prefix: number;
-
- /**
- * IP address constructor.
- *
- * @internal
- */
- public readonly _AddressClass: IP.Class;
-
- /**
- * Creates a new subnet instance.
- *
- * @param address IP address.
- * @param prefix Bit length of the subnet prefix.
- * @throws {@link !RangeError} If the prefix is greater than the IP address family bit length, or if negative.
- */
- public constructor(address: T, prefix: number) {
- this._AddressClass = address.constructor as IP.Class;
- if (prefix < 0)
- throw new RangeError("Expected positive prefix, got " + prefix);
- if (prefix > this._AddressClass.BIT_LENGTH)
- throw new RangeError(`Expected prefix less than ${this._AddressClass.BIT_LENGTH}, got ${prefix}`);
- this.prefix = prefix;
- this.address = new this._AddressClass(address.value & this.netmask()) as T;
+ /**
+ * Subnet for IPv4-mapped IPv6 addresses (`::ffff:0.0.0.0/96`).
+ */
+ public static readonly IPV4_MAPPED_IPV6 = Subnet.fromCIDR(
+ "::ffff:0.0.0.0/96",
+ );
+
+ /**
+ * Subnet address (i.e. the first IP address of the network).
+ */
+ public readonly address: T;
+
+ /**
+ * Bit length of the subnet prefix.
+ */
+ public readonly prefix: number;
+
+ /**
+ * IP address constructor.
+ *
+ * @internal
+ */
+ public readonly _AddressClass: IP.Class;
+
+ /**
+ * Creates a new subnet instance.
+ *
+ * @param address IP address.
+ * @param prefix Bit length of the subnet prefix.
+ * @throws {@link !RangeError} If the prefix is greater than the IP address family bit length, or if negative.
+ */
+ public constructor(address: T, prefix: number) {
+ this._AddressClass = address.constructor as IP.Class;
+ if (prefix < 0) {
+ throw new RangeError("Expected positive prefix, got " + prefix);
}
-
- /**
- * Creates a subnet from a string in CIDR notation.
- *
- * @param cidr String in CIDR notation.
- * @throws {@link !RangeError} If the address or prefix is invalid.
- */
- public static fromCIDR(cidr: string): Subnet {
- const parts = cidr.split("/", 2);
- if (parts.length !== 2) throw new RangeError("Expected CIDR notation, got " + cidr);
- const prefix = Number.parseInt(parts[1]!, 10);
- if (Number.isNaN(prefix)) throw new RangeError("Expected numeric prefix, got " + parts[1]);
- return new Subnet(IPAddress.fromString(parts[0]!) as T, prefix);
+ if (prefix > this._AddressClass.BIT_LENGTH) {
+ throw new RangeError(
+ `Expected prefix less than ${this._AddressClass.BIT_LENGTH}, got ${prefix}`,
+ );
}
-
- /**
- * Creates an array of subnets to represent an arbitrary range of IP addresses.
- *
- * @param start Starting IP address.
- * @param end Ending IP address.
- */
- public static range(start: T, end: T): Subnet[] {
- const AddressClass = start.constructor as IP.Class;
- const result: Subnet[] = [];
-
- let current = start.value;
-
- while (current <= end.value) {
- let tz = 0;
- while (tz < AddressClass.BIT_LENGTH && ((current >> BigInt(tz)) & 1n) === 0n)
- ++tz;
- const prefixAlign = AddressClass.BIT_LENGTH - tz;
-
- let remaining = end.value - current + 1n;
- let maxBlockBits = 0;
- while (remaining > 1n) {
- remaining >>= 1n;
- ++maxBlockBits;
- }
- const prefixRange = AddressClass.BIT_LENGTH - maxBlockBits;
-
- const subnet = new Subnet(new AddressClass(current) as T, Math.max(prefixAlign, prefixRange));
- result.push(subnet);
- current += subnet.size();
- }
-
- return result;
+ this.prefix = prefix;
+ this.address = new this._AddressClass(address.value & this.netmask()) as T;
+ }
+
+ /**
+ * Creates a subnet from a string in CIDR notation.
+ *
+ * @param cidr String in CIDR notation.
+ * @throws {@link !RangeError} If the address or prefix is invalid.
+ */
+ public static fromCIDR(cidr: string): Subnet {
+ const parts = cidr.split("/", 2);
+ if (parts.length !== 2) {
+ throw new RangeError("Expected CIDR notation, got " + cidr);
}
-
- /**
- * Returns the network mask of this subnet.
- */
- public netmask(): bigint {
- return ((1n << BigInt(this.prefix)) - 1n) << BigInt(this._AddressClass.BIT_LENGTH - this.prefix);
+ const prefix = Number.parseInt(parts[1]!, 10);
+ if (Number.isNaN(prefix)) {
+ throw new RangeError("Expected numeric prefix, got " + parts[1]);
}
-
- /**
- * Returns the wildcard (host) mask—the inverse of the {@link netmask}.
- */
- public wildcard(): bigint {
- return (1n << BigInt(this._AddressClass.BIT_LENGTH - this.prefix)) - 1n;
+ return new Subnet(IPAddress.fromString(parts[0]!) as T, prefix);
+ }
+
+ /**
+ * Creates an array of subnets to represent an arbitrary range of IP addresses.
+ *
+ * @param start Starting IP address.
+ * @param end Ending IP address.
+ */
+ public static range(start: T, end: T): Subnet[] {
+ const AddressClass = start.constructor as IP.Class;
+ const result: Subnet[] = [];
+
+ let current = start.value;
+
+ while (current <= end.value) {
+ let tz = 0;
+ while (
+ tz < AddressClass.BIT_LENGTH && ((current >> BigInt(tz)) & 1n) === 0n
+ ) {
+ ++tz;
+ }
+ const prefixAlign = AddressClass.BIT_LENGTH - tz;
+
+ let remaining = end.value - current + 1n;
+ let maxBlockBits = 0;
+ while (remaining > 1n) {
+ remaining >>= 1n;
+ ++maxBlockBits;
+ }
+ const prefixRange = AddressClass.BIT_LENGTH - maxBlockBits;
+
+ const subnet = new Subnet(
+ new AddressClass(current) as T,
+ Math.max(prefixAlign, prefixRange),
+ );
+ result.push(subnet);
+ current += subnet.size();
}
- /**
- * Returns the address at the specific index.
- *
- * @param index Zero-based index of the address to be returned. Negative index counts from the end of the subnet.
- * @returns The address in the subnet matching the given index. Always returns `undefined` if `index < -size()` or
- * `index >= size()`.
- */
- public at(index: 0 | 0n | -1 | -1n): T;
-
- /**
- * Returns the address at the specific index.
- *
- * @param index Zero-based index of the address to be returned. Negative index counts from the end of the subnet.
- * @returns The address in the subnet matching the given index. Always returns `undefined` if `index < -size()` or
- * `index >= size()`.
- */
- public at(index: bigint): T | undefined;
-
- /**
- * Returns the address at the specific index.
- *
- * @param index Zero-based index of the address to be returned. Negative index counts from the end of the subnet.
- * @returns The address in the subnet matching the given index. Always returns `undefined` if `index < -size()` or
- * `index >= size()`.
- */
- public at(index: number): T | undefined;
-
- public at(a: bigint | number): T | undefined {
- const index = BigInt(a);
- if (index < -this.size() || index >= this.size()) return undefined;
- if (index < 0)
- return this.at(this.size() + index);
- return new this._AddressClass(this.address.value + BigInt(index)) as T;
+ return result;
+ }
+
+ /**
+ * Returns the network mask of this subnet.
+ */
+ public netmask(): bigint {
+ return ((1n << BigInt(this.prefix)) - 1n) <<
+ BigInt(this._AddressClass.BIT_LENGTH - this.prefix);
+ }
+
+ /**
+ * Returns the wildcard (host) mask—the inverse of the {@link netmask}.
+ */
+ public wildcard(): bigint {
+ return (1n << BigInt(this._AddressClass.BIT_LENGTH - this.prefix)) - 1n;
+ }
+
+ /**
+ * Returns the address at the specific index.
+ *
+ * @param index Zero-based index of the address to be returned. Negative index counts from the end of the subnet.
+ * @returns The address in the subnet matching the given index. Always returns `undefined` if `index < -size()` or
+ * `index >= size()`.
+ */
+ public at(index: 0 | 0n | -1 | -1n): T;
+
+ /**
+ * Returns the address at the specific index.
+ *
+ * @param index Zero-based index of the address to be returned. Negative index counts from the end of the subnet.
+ * @returns The address in the subnet matching the given index. Always returns `undefined` if `index < -size()` or
+ * `index >= size()`.
+ */
+ public at(index: bigint): T | undefined;
+
+ /**
+ * Returns the address at the specific index.
+ *
+ * @param index Zero-based index of the address to be returned. Negative index counts from the end of the subnet.
+ * @returns The address in the subnet matching the given index. Always returns `undefined` if `index < -size()` or
+ * `index >= size()`.
+ */
+ public at(index: number): T | undefined;
+
+ public at(a: bigint | number): T | undefined {
+ const index = BigInt(a);
+ if (index < -this.size() || index >= this.size()) return undefined;
+ if (index < 0) {
+ return this.at(this.size() + index);
}
-
- /**
- * Determines whether the provided address is contained within this subnet.
- *
- * @param address IP address to check.
- */
- public contains(address: T): boolean {
- return address instanceof this._AddressClass && (address.value & this.netmask()) === this.address.value;
+ return new this._AddressClass(this.address.value + BigInt(index)) as T;
+ }
+
+ /**
+ * Determines whether the provided address is contained within this subnet.
+ *
+ * @param address IP address to check.
+ */
+ public contains(address: T): boolean {
+ return address instanceof this._AddressClass &&
+ (address.value & this.netmask()) === this.address.value;
+ }
+
+ /**
+ * Determines whether the provided subnet is fully contained within this subnet.
+ *
+ * @param subnet Subnet to check.
+ */
+ public containsSubnet(subnet: Subnet): boolean {
+ return this.prefix <= subnet.prefix && this.contains(subnet.address);
+ }
+
+ /**
+ * Returns the exact number of addresses in this subnet.
+ */
+ public size(): bigint {
+ return this.wildcard() + 1n;
+ }
+
+ /**
+ * Iterates all IP addresses in this subnet.
+ *
+ * **NOTE**: This can be slow for large subnets.
+ */
+ public *addresses(): IterableIterator {
+ const end = this.address.value + this.size();
+ for (let i = this.address.value; i < end; ++i) {
+ yield new this._AddressClass(i) as T;
}
-
- /**
- * Determines whether the provided subnet is fully contained within this subnet.
- *
- * @param subnet Subnet to check.
- */
- public containsSubnet(subnet: Subnet): boolean {
- return this.prefix <= subnet.prefix && this.contains(subnet.address);
+ }
+
+ /**
+ * Iterates all IP addresses in this subnet.
+ *
+ * **NOTE**: This can be slow for large subnets.
+ */
+ public [Symbol.iterator](): Iterator {
+ return this.addresses();
+ }
+
+ /**
+ * Creates a set containing all IP addresses in this subnet.
+ *
+ * **NOTE**: This can be slow for large subnets.
+ */
+ public set(): Set {
+ return new Set(this.addresses());
+ }
+
+ /**
+ * Returns the string representation of this subnet in CIDR notation.
+ *
+ * @example "203.0.113.0/24"
+ */
+ public toString(): string {
+ return this.address.toString() + "/" + this.prefix;
+ }
+
+ /**
+ * Checks if this subnet is adjacent to another subnet.
+ *
+ * @param subnet Subnet to check.
+ * @throws {@link !TypeError} If the provided subnet is not of the same family.
+ */
+ public isAdjacent(subnet: Subnet): boolean {
+ if (this._AddressClass !== subnet._AddressClass) {
+ throw new TypeError(
+ `Expected ${this._AddressClass.name} subnet, but got ${subnet._AddressClass.name}.`,
+ );
}
- /**
- * Returns the exact number of addresses in this subnet.
- */
- public size(): bigint {
- return this.wildcard() + 1n;
+ return (this.address.value + this.size() === subnet.address.value) ||
+ (subnet.address.value + subnet.size() === this.address.value);
+ }
+
+ /**
+ * Checks whether another subnet can be merged with this subnet.
+ *
+ * @param subnet Subnet to check.
+ */
+ public canMerge(subnet: Subnet): boolean {
+ if (this.prefix !== subnet.prefix) {
+ return false;
}
-
- /**
- * Iterates all IP addresses in this subnet.
- *
- * **NOTE**: This can be slow for large subnets.
- */
- public* addresses(): IterableIterator {
- const end = this.address.value + this.size();
- for (let i = this.address.value; i < end; ++i)
- yield new this._AddressClass(i) as T;
+ try {
+ return this.isAdjacent(subnet);
+ } catch {
+ return false;
}
-
- /**
- * Iterates all IP addresses in this subnet.
- *
- * **NOTE**: This can be slow for large subnets.
- */
- public [Symbol.iterator](): Iterator {
- return this.addresses();
+ }
+
+ /**
+ * Creates a larger subnet by merging with an adjacent subnet of the same family and size.
+ *
+ * @param subnet Subnet to merge with.
+ * @throws {@link !TypeError} If the subnet is not of the same family or size.
+ * @throws {@link !RangeError} If the subnet is not adjacent to this subnet.
+ */
+ public merge(subnet: Subnet): Subnet {
+ if (!this.isAdjacent(subnet)) {
+ throw new RangeError(
+ `${subnet.toString()} is not adjacent to ${this.toString()}.`,
+ );
}
-
- /**
- * Creates a set containing all IP addresses in this subnet.
- *
- * **NOTE**: This can be slow for large subnets.
- */
- public set(): Set {
- return new Set(this.addresses());
+ if (this.prefix !== subnet.prefix) {
+ throw new TypeError(
+ `Expected /${this.prefix} subnet, but got /${subnet.prefix}.`,
+ );
}
- /**
- * Returns the string representation of this subnet in CIDR notation.
- *
- * @example "203.0.113.0/24"
- */
- public toString(): string {
- return this.address.toString() + "/" + this.prefix;
+ return new Subnet(
+ this.address.value < subnet.address.value ? this.address : subnet.address,
+ this.prefix - 1,
+ );
+ }
+
+ /**
+ * Splits this subnet into as many subnets of the specified prefix length as possible.
+ *
+ * @param prefix Prefix length of the resulting subnets.
+ * @throws {@link !RangeError} If the prefix is smaller than the current prefix, or over the IP address
+ * family bit length.
+ */
+ public split(prefix: number): Subnet[] {
+ if (prefix < this.prefix || prefix > this._AddressClass.BIT_LENGTH) {
+ throw new RangeError(
+ `Expected prefix in the range [${
+ this.prefix + 1
+ }, ${this._AddressClass.BIT_LENGTH}], got ${prefix}.`,
+ );
}
- /**
- * Checks if this subnet is adjacent to another subnet.
- *
- * @param subnet Subnet to check.
- * @throws {@link !TypeError} If the provided subnet is not of the same family.
- */
- public isAdjacent(subnet: Subnet): boolean {
- if (this._AddressClass !== subnet._AddressClass)
- throw new TypeError(`Expected ${this._AddressClass.name} subnet, but got ${subnet._AddressClass.name}.`);
-
- return (this.address.value + this.size() === subnet.address.value)
- || (subnet.address.value + subnet.size() === this.address.value);
+ const length = 1 << (prefix - this.prefix);
+ const size = this.size() / BigInt(length);
+
+ return Array.from({ length }, (_, i) =>
+ new Subnet(
+ new this._AddressClass(this.address.value + BigInt(i) * size) as T,
+ prefix,
+ ));
+ }
+
+ /**
+ * Subtracts a subnet from this subnet.
+ *
+ * @param subnet Subnet to exclude.
+ * @throws {@link !TypeError} If the subnet is not of the same family.
+ * @returns An array of subnets representing the portions of this subnet that do not overlap with the given subnet.
+ * Returns an empty array if the given subnet fully covers this subnet.
+ */
+ public subtract(subnet: Subnet): Subnet[] {
+ if (this._AddressClass !== subnet._AddressClass) {
+ throw new TypeError(
+ `Expected ${this._AddressClass.name} subnet, but got ${subnet._AddressClass.name}.`,
+ );
}
- /**
- * Checks whether another subnet can be merged with this subnet.
- *
- * @param subnet Subnet to check.
- */
- public canMerge(subnet: Subnet): boolean {
- if (this.prefix !== subnet.prefix)
- return false;
- try {
- return this.isAdjacent(subnet);
- }
- catch {
- return false;
- }
+ if (subnet.containsSubnet(this)) {
+ return [];
}
- /**
- * Creates a larger subnet by merging with an adjacent subnet of the same family and size.
- *
- * @param subnet Subnet to merge with.
- * @throws {@link !TypeError} If the subnet is not of the same family or size.
- * @throws {@link !RangeError} If the subnet is not adjacent to this subnet.
- */
- public merge(subnet: Subnet): Subnet {
- if (!this.isAdjacent(subnet))
- throw new RangeError(`${subnet.toString()} is not adjacent to ${this.toString()}.`);
- if (this.prefix !== subnet.prefix)
- throw new TypeError(`Expected /${this.prefix} subnet, but got /${subnet.prefix}.`);
-
- return new Subnet(
- this.address.value < subnet.address.value
- ? this.address
- : subnet.address,
- this.prefix - 1,
- );
+ if (!this.containsSubnet(subnet)) {
+ return [this];
}
- /**
- * Splits this subnet into as many subnets of the specified prefix length as possible.
- *
- * @param prefix Prefix length of the resulting subnets.
- * @throws {@link !RangeError} If the prefix is smaller than the current prefix, or over the IP address
- * family bit length.
- */
- public split(prefix: number): Subnet[] {
- if (prefix < this.prefix || prefix > this._AddressClass.BIT_LENGTH)
- throw new RangeError(
- `Expected prefix in the range [${this.prefix + 1}, ${this._AddressClass.BIT_LENGTH}], got ${prefix}.`,
- );
-
- const length = 1 << (prefix - this.prefix);
- const size = this.size() / BigInt(length);
-
- return Array.from({length}, (_, i) => new Subnet(
- new this._AddressClass(this.address.value + BigInt(i) * size) as T,
- prefix,
- ));
- }
-
- /**
- * Subtracts a subnet from this subnet.
- *
- * @param subnet Subnet to exclude.
- * @throws {@link !TypeError} If the subnet is not of the same family.
- * @returns An array of subnets representing the portions of this subnet that do not overlap with the given subnet.
- * Returns an empty array if the given subnet fully covers this subnet.
- */
- public subtract(subnet: Subnet): Subnet[] {
- if (this._AddressClass !== subnet._AddressClass)
- throw new TypeError(`Expected ${this._AddressClass.name} subnet, but got ${subnet._AddressClass.name}.`);
-
- if (subnet.containsSubnet(this))
- return [];
-
- if (!this.containsSubnet(subnet))
- return [this];
+ const result: Subnet[] = [];
- const result: Subnet[] = [];
-
- if (this.address.value < subnet.address.value)
- result.push(...Subnet.range(this.address, subnet.address.offset(-1) as T));
-
- if (subnet.at(-1).value < this.at(-1).value)
- result.push(...Subnet.range(subnet.at(-1).offset(1) as T, this.at(-1)));
-
- return result;
+ if (this.address.value < subnet.address.value) {
+ result.push(
+ ...Subnet.range(this.address, subnet.address.offset(-1) as T),
+ );
}
- /**
- * Checks if the given subnets are equal.
- *
- * @param subnet Subnet to compare.
- */
- public equals(subnet: Subnet): boolean {
- return this._AddressClass === subnet._AddressClass
- && this.address.equals(subnet.address)
- && this.prefix === subnet.prefix;
+ if (subnet.at(-1).value < this.at(-1).value) {
+ result.push(...Subnet.range(subnet.at(-1).offset(1) as T, this.at(-1)));
}
+
+ return result;
+ }
+
+ /**
+ * Checks if the given subnets are equal.
+ *
+ * @param subnet Subnet to compare.
+ */
+ public equals(subnet: Subnet): boolean {
+ return this._AddressClass === subnet._AddressClass &&
+ this.address.equals(subnet.address) &&
+ this.prefix === subnet.prefix;
+ }
}
diff --git a/src/SubnetList.ts b/src/SubnetList.ts
index 85452e8..9b94e33 100644
--- a/src/SubnetList.ts
+++ b/src/SubnetList.ts
@@ -14,195 +14,201 @@
* You should have received a copy of the GNU Lesser General Public License along with @cldn/ip.
* If not, see .
*/
-import {IP, IPAddress, Network, Subnet} from "./index.js";
+import { IP, IPAddress, Network, Subnet } from "./index.js";
/**
* Represents a network comprised of subnets.
*/
export class SubnetList implements Network {
- /**
- * A network of reserved subnets. This network does not contain publicly routable IP addresses.
- */
- public static readonly BOGON = new SubnetList([
- // IPv4
- Subnet.fromCIDR("0.0.0.0/8"),
- Subnet.fromCIDR("10.0.0.0/8"),
- Subnet.fromCIDR("100.64.0.0/10"),
- Subnet.fromCIDR("127.0.0.0/8"),
- Subnet.fromCIDR("169.254.0.0/16"),
- Subnet.fromCIDR("172.16.0.0/12"),
- Subnet.fromCIDR("192.0.0.0/24"),
- Subnet.fromCIDR("192.0.2.0/24"),
- Subnet.fromCIDR("192.88.99.0/24"),
- Subnet.fromCIDR("192.168.0.0/16"),
- Subnet.fromCIDR("198.18.0.0/15"),
- Subnet.fromCIDR("198.51.100.0/24"),
- Subnet.fromCIDR("203.0.113.0/24"),
- Subnet.fromCIDR("224.0.0.0/4"),
- Subnet.fromCIDR("233.252.0.0/24"),
- Subnet.fromCIDR("240.0.0.0/4"),
- Subnet.fromCIDR("255.255.255.255/32"),
- // IPv6
- Subnet.fromCIDR("::/128"),
- Subnet.fromCIDR("::1/128"),
- Subnet.fromCIDR("64:ff9b:1::/48"),
- Subnet.fromCIDR("100::/64"),
- Subnet.fromCIDR("2001:20::/28"),
- Subnet.fromCIDR("2001:db8::/32"),
- Subnet.fromCIDR("3fff::/20"),
- Subnet.fromCIDR("5f00::/16"),
- Subnet.fromCIDR("fc00::/7"),
- Subnet.fromCIDR("fe80::/10"),
- ]);
-
- readonly #subnets: Subnet[] = [];
-
- /**
- * Creates a new network.
- *
- * @param [subnets] Initial subnets to add to this network.
- */
- public constructor(subnets?: Iterable>) {
- if (subnets)
- for (const subnet of subnets)
- this.add(subnet);
+ /**
+ * A network of reserved subnets. This network does not contain publicly routable IP addresses.
+ */
+ public static readonly BOGON = new SubnetList([
+ // IPv4
+ Subnet.fromCIDR("0.0.0.0/8"),
+ Subnet.fromCIDR("10.0.0.0/8"),
+ Subnet.fromCIDR("100.64.0.0/10"),
+ Subnet.fromCIDR("127.0.0.0/8"),
+ Subnet.fromCIDR("169.254.0.0/16"),
+ Subnet.fromCIDR("172.16.0.0/12"),
+ Subnet.fromCIDR("192.0.0.0/24"),
+ Subnet.fromCIDR("192.0.2.0/24"),
+ Subnet.fromCIDR("192.88.99.0/24"),
+ Subnet.fromCIDR("192.168.0.0/16"),
+ Subnet.fromCIDR("198.18.0.0/15"),
+ Subnet.fromCIDR("198.51.100.0/24"),
+ Subnet.fromCIDR("203.0.113.0/24"),
+ Subnet.fromCIDR("224.0.0.0/4"),
+ Subnet.fromCIDR("233.252.0.0/24"),
+ Subnet.fromCIDR("240.0.0.0/4"),
+ Subnet.fromCIDR("255.255.255.255/32"),
+ // IPv6
+ Subnet.fromCIDR("::/128"),
+ Subnet.fromCIDR("::1/128"),
+ Subnet.fromCIDR("64:ff9b:1::/48"),
+ Subnet.fromCIDR("100::/64"),
+ Subnet.fromCIDR("2001:20::/28"),
+ Subnet.fromCIDR("2001:db8::/32"),
+ Subnet.fromCIDR("3fff::/20"),
+ Subnet.fromCIDR("5f00::/16"),
+ Subnet.fromCIDR("fc00::/7"),
+ Subnet.fromCIDR("fe80::/10"),
+ ]);
+
+ readonly #subnets: Subnet[] = [];
+
+ /**
+ * Creates a new network.
+ *
+ * @param [subnets] Initial subnets to add to this network.
+ */
+ public constructor(subnets?: Iterable>) {
+ if (subnets) {
+ for (const subnet of subnets) {
+ this.add(subnet);
+ }
}
+ }
- public* [Symbol.iterator](): Iterator {
- for (const subnet of this.#subnets.values())
- for (const address of subnet)
- yield address;
+ public *[Symbol.iterator](): Iterator {
+ for (const subnet of this.#subnets.values()) {
+ for (const address of subnet) {
+ yield address;
+ }
}
-
- /**
- * Adds a subnet to this network.
- *
- * @param subnet Subnet to add.
- * @returns Whether the subnet was not already fully in the network.
- */
- public add(subnet: Subnet): boolean;
-
- /**
- * Adds an IP address to this network.
- *
- * @param ip IP address to add.
- * @returns Whether the IP address was not already in the network.
- */
- public add(ip: IP): boolean;
-
- /**
- * Adds a subnet list to this network.
- *
- * @param subnets Network of subnets to add.
- */
- public add(subnets: SubnetList): void;
-
- public add(a: Subnet | IP | SubnetList): boolean | void {
- if (a instanceof Subnet) {
- if (this.hasSubnet(a)) return false;
-
- const subSubnet = this.#subnets.findIndex(s => a.containsSubnet(s));
- if (subSubnet !== -1) {
- this.#subnets.splice(subSubnet, 1, a);
- this.optimise();
- return true;
- }
-
- this.#subnets.push(a);
- this.optimise();
- return true;
- }
- else if (a instanceof IPAddress) {
- const Class = a.constructor as IP.Class;
- return this.add(new Subnet(a, Class.BIT_LENGTH));
- }
- else {
- for (const subnet of a.#subnets)
- this.add(subnet);
- this.optimise();
- return;
- }
- }
-
- /**
- * Removes a subnet from this network.
- *
- * @param subnet Subnet to remove.
- * @returns Whether the subnet was found and removed.
- */
- public remove(subnet: Subnet) {
- for (const [index, s] of this.#subnets.entries()) {
- if (s.equals(subnet)) {
- this.#subnets.splice(index, 1);
- return true;
- }
- if (s.containsSubnet(subnet)) {
- this.#subnets.splice(index, 1, ...s.subtract(subnet));
- return true;
- }
- }
- return false;
+ }
+
+ /**
+ * Adds a subnet to this network.
+ *
+ * @param subnet Subnet to add.
+ * @returns Whether the subnet was not already fully in the network.
+ */
+ public add(subnet: Subnet): boolean;
+
+ /**
+ * Adds an IP address to this network.
+ *
+ * @param ip IP address to add.
+ * @returns Whether the IP address was not already in the network.
+ */
+ public add(ip: IP): boolean;
+
+ /**
+ * Adds a subnet list to this network.
+ *
+ * @param subnets Network of subnets to add.
+ */
+ public add(subnets: SubnetList): void;
+
+ public add(a: Subnet | IP | SubnetList): boolean | void {
+ if (a instanceof Subnet) {
+ if (this.hasSubnet(a)) return false;
+
+ const subSubnet = this.#subnets.findIndex((s) => a.containsSubnet(s));
+ if (subSubnet !== -1) {
+ this.#subnets.splice(subSubnet, 1, a);
+ this.optimise();
+ return true;
+ }
+
+ this.#subnets.push(a);
+ this.optimise();
+ return true;
+ } else if (a instanceof IPAddress) {
+ const Class = a.constructor as IP.Class;
+ return this.add(new Subnet(a, Class.BIT_LENGTH));
+ } else {
+ for (const subnet of a.#subnets) {
+ this.add(subnet);
+ }
+ this.optimise();
+ return;
}
-
- /**
- * Checks if a subnet is in this network.
- *
- * @param subnet Subnet to check.
- */
- public hasSubnet(subnet: Subnet): boolean {
- return this.#subnets.some(s => s.containsSubnet(subnet));
+ }
+
+ /**
+ * Removes a subnet from this network.
+ *
+ * @param subnet Subnet to remove.
+ * @returns Whether the subnet was found and removed.
+ */
+ public remove(subnet: Subnet) {
+ for (const [index, s] of this.#subnets.entries()) {
+ if (s.equals(subnet)) {
+ this.#subnets.splice(index, 1);
+ return true;
+ }
+ if (s.containsSubnet(subnet)) {
+ this.#subnets.splice(index, 1, ...s.subtract(subnet));
+ return true;
+ }
}
-
- /**
- * Returns all subnets in this network.
- */
- public subnets(): Subnet[] {
- return Array.from(this.#subnets);
- }
-
- /**
- * Checks if an IP address is in this network.
- *
- * @param address IP address to check.
- */
- public contains(address: IP): boolean {
- for (const subnet of this.#subnets.values()) if (subnet.contains(address)) return true;
- return false;
+ return false;
+ }
+
+ /**
+ * Checks if a subnet is in this network.
+ *
+ * @param subnet Subnet to check.
+ */
+ public hasSubnet(subnet: Subnet): boolean {
+ return this.#subnets.some((s) => s.containsSubnet(subnet));
+ }
+
+ /**
+ * Returns all subnets in this network.
+ */
+ public subnets(): Subnet[] {
+ return Array.from(this.#subnets);
+ }
+
+ /**
+ * Checks if an IP address is in this network.
+ *
+ * @param address IP address to check.
+ */
+ public contains(address: IP): boolean {
+ for (const subnet of this.#subnets.values()) {
+ if (subnet.contains(address)) return true;
}
-
- /**
- * Returns the number of addresses in this network.
- */
- public size(): bigint {
- let size = 0n;
- for (const subnet of this.#subnets.values()) size += subnet.size();
- return size;
+ return false;
+ }
+
+ /**
+ * Returns the number of addresses in this network.
+ */
+ public size(): bigint {
+ let size = 0n;
+ for (const subnet of this.#subnets.values()) size += subnet.size();
+ return size;
+ }
+
+ /**
+ * Optimises memory by merging adjacent subnets.
+ */
+ private optimise(): void {
+ this.#subnets.sort((a, b) =>
+ a._AddressClass.BIT_LENGTH - b._AddressClass.BIT_LENGTH ||
+ b.prefix - a.prefix ||
+ (a.address.value < b.address.value ? -1 : 1)
+ );
+
+ let merged = false;
+
+ for (let i = 0; i + 1 < this.#subnets.length;) {
+ const left = this.#subnets.at(i)!;
+ const right = this.#subnets.at(i + 1)!;
+
+ if (!left.canMerge(right)) {
+ ++i;
+ continue;
+ }
+
+ merged = true;
+ this.#subnets.splice(i, 2, left.merge(right));
}
- /**
- * Optimises memory by merging adjacent subnets.
- */
- private optimise(): void {
- this.#subnets.sort((a, b) => a._AddressClass.BIT_LENGTH - b._AddressClass.BIT_LENGTH
- || b.prefix - a.prefix
- || (a.address.value < b.address.value ? -1 : 1),
- );
-
- let merged = false;
-
- for (let i = 0; i + 1 < this.#subnets.length;) {
- const left = this.#subnets.at(i)!;
- const right = this.#subnets.at(i + 1)!;
-
- if (!left.canMerge(right)) {
- ++i;
- continue;
- }
-
- merged = true;
- this.#subnets.splice(i, 2, left.merge(right));
- }
-
- if (merged) this.optimise();
- }
+ if (merged) this.optimise();
+ }
}
diff --git a/src/index.ts b/src/index.ts
index f39bb48..6e9e183 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,10 +15,10 @@
* If not, see .
*/
-export type {IP} from "./IP.js";
-export {IPAddress} from "./IPAddress.js";
-export {IPv4} from "./IPv4.js";
-export {IPv6} from "./IPv6.js";
-export type {Network} from "./Network.js";
-export {Subnet} from "./Subnet.js";
-export {SubnetList} from "./SubnetList.js";
+export type { IP } from "./IP.js";
+export { IPAddress } from "./IPAddress.js";
+export { IPv4 } from "./IPv4.js";
+export { IPv6 } from "./IPv6.js";
+export type { Network } from "./Network.js";
+export { Subnet } from "./Subnet.js";
+export { SubnetList } from "./SubnetList.js";
diff --git a/tests/IPAddress.test.ts b/tests/IPAddress.test.ts
index 5fb55b0..094b6a8 100644
--- a/tests/IPAddress.test.ts
+++ b/tests/IPAddress.test.ts
@@ -1,49 +1,49 @@
-import {describe, expect, it} from "vitest";
-import {IPAddress, IPv4, IPv6} from "../src/index.js";
+import { describe, expect, it } from "vitest";
+import { IPAddress, IPv4, IPv6 } from "../src/index.js";
describe("IPAddress", () => {
- describe("static fromString", () => {
- it("creates IPv4 from IPv4 string", () => {
- const ip = IPAddress.fromString("192.0.2.0");
- expect(ip).toBeInstanceOf(IPv4);
- expect(ip.toString()).toBe("192.0.2.0");
- });
+ describe("static fromString", () => {
+ it("creates IPv4 from IPv4 string", () => {
+ const ip = IPAddress.fromString("192.0.2.0");
+ expect(ip).toBeInstanceOf(IPv4);
+ expect(ip.toString()).toBe("192.0.2.0");
+ });
- it("creates IPv6 from IPv6 string", () => {
- const ip = IPAddress.fromString("2001:db8::");
- expect(ip).toBeInstanceOf(IPv6);
- expect(ip.toString()).toBe("2001:db8::");
- });
+ it("creates IPv6 from IPv6 string", () => {
+ const ip = IPAddress.fromString("2001:db8::");
+ expect(ip).toBeInstanceOf(IPv6);
+ expect(ip.toString()).toBe("2001:db8::");
+ });
- it("creates IPv4 from IPv4 string with resolveMapped=false", () => {
- const ip = IPAddress.fromString("192.0.2.0");
- expect(ip).toBeInstanceOf(IPv4);
- expect(ip.toString()).toBe("192.0.2.0");
- });
+ it("creates IPv4 from IPv4 string with resolveMapped=false", () => {
+ const ip = IPAddress.fromString("192.0.2.0");
+ expect(ip).toBeInstanceOf(IPv4);
+ expect(ip.toString()).toBe("192.0.2.0");
+ });
- it("creates IPv6 from IPv6 string with resolveMapped=true", () => {
- const ip = IPAddress.fromString("2001:db8::", true);
- expect(ip).toBeInstanceOf(IPv6);
- expect(ip.toString()).toBe("2001:db8::");
- });
+ it("creates IPv6 from IPv6 string with resolveMapped=true", () => {
+ const ip = IPAddress.fromString("2001:db8::", true);
+ expect(ip).toBeInstanceOf(IPv6);
+ expect(ip.toString()).toBe("2001:db8::");
+ });
- it("creates IPv4 from IPv4-mapped IPv6 string with resolveMapped=true", () => {
- const ip = IPAddress.fromString("::ffff:192.0.2.0", true);
- expect(ip).toBeInstanceOf(IPv4);
- expect(ip.toString()).toBe("192.0.2.0");
- });
+ it("creates IPv4 from IPv4-mapped IPv6 string with resolveMapped=true", () => {
+ const ip = IPAddress.fromString("::ffff:192.0.2.0", true);
+ expect(ip).toBeInstanceOf(IPv4);
+ expect(ip.toString()).toBe("192.0.2.0");
+ });
- it("creates IPv6 from IPv4-mapped IPv6 string with resolveMapped=false", () => {
- const ip = IPAddress.fromString("::ffff:192.0.2.0");
- expect(ip).toBeInstanceOf(IPv6);
- expect(ip.toString()).toBe("::ffff:c000:200");
- });
+ it("creates IPv6 from IPv4-mapped IPv6 string with resolveMapped=false", () => {
+ const ip = IPAddress.fromString("::ffff:192.0.2.0");
+ expect(ip).toBeInstanceOf(IPv6);
+ expect(ip.toString()).toBe("::ffff:c000:200");
+ });
- it("throws RangeError for invalid string", () => {
- expect(() => IPAddress.fromString("not an ip")).toThrow(RangeError);
- expect(() => IPAddress.fromString("invalid:ip")).toThrow(RangeError);
- expect(() => IPAddress.fromString("not.an.ip")).toThrow(RangeError);
- expect(() => IPAddress.fromString("")).toThrow(RangeError);
- });
+ it("throws RangeError for invalid string", () => {
+ expect(() => IPAddress.fromString("not an ip")).toThrow(RangeError);
+ expect(() => IPAddress.fromString("invalid:ip")).toThrow(RangeError);
+ expect(() => IPAddress.fromString("not.an.ip")).toThrow(RangeError);
+ expect(() => IPAddress.fromString("")).toThrow(RangeError);
});
+ });
});
diff --git a/tests/IPv4.test.ts b/tests/IPv4.test.ts
index 9794135..7732141 100644
--- a/tests/IPv4.test.ts
+++ b/tests/IPv4.test.ts
@@ -1,150 +1,154 @@
-import {describe, expect, it} from "vitest";
-import {IPv4, IPv6} from "../src/index.js";
+import { describe, expect, it } from "vitest";
+import { IPv4, IPv6 } from "../src/index.js";
describe("IPv4", () => {
- describe("static BIT_LENGTH", () => {
- it("is 32", () => {
- expect(IPv4.BIT_LENGTH).toBe(32);
- });
- });
-
- describe("constructor", () => {
- it("constructs from a valid number", () => {
- const ip = new IPv4(0xC0000200);
- expect(ip.value).toBe(0xC0000200n);
- expect(ip.toString()).toBe("192.0.2.0");
- });
-
- it("constructs from a valid bigint", () => {
- const ip = new IPv4(0xC6336410n);
- expect(ip.value).toBe(0xC6336410n);
- expect(ip.toString()).toBe("198.51.100.16");
- });
-
- it("constructs from 0", () => {
- const ip = new IPv4(0);
- expect(ip.value).toBe(0n);
- expect(ip.toString()).toBe("0.0.0.0");
- });
-
- it("constructs from 2^32 - 1", () => {
- const ip = new IPv4(2 ** 32 - 1);
- expect(ip.value).toBe(2n ** 32n - 1n);
- expect(ip.toString()).toBe("255.255.255.255");
- });
-
- it("throws TypeError for value < 0", () => {
- expect(() => new IPv4(-1)).toThrow(TypeError);
- expect(() => new IPv4(-1n)).toThrow(TypeError);
- });
-
- it("throws TypeError for value > 2^32 - 1", () => {
- expect(() => new IPv4(2 ** 32)).toThrow(TypeError);
- expect(() => new IPv4(2n ** 32n)).toThrow(TypeError);
- });
- });
-
- describe("static fromBinary", () => {
- it("creates IPv4 from 4-octet array", () => {
- const ip = IPv4.fromBinary(new Uint8Array([192, 0, 2, 0]));
- expect(ip.toString()).toBe("192.0.2.0");
- });
-
- it("throws RangeError if array has less than 4 octets", () => {
- expect(() => IPv4.fromBinary(new Uint8Array([192, 0, 2]))).toThrow(RangeError);
- });
-
- it("throws RangeError if array has more than 4 octets", () => {
- expect(() => IPv4.fromBinary(new Uint8Array([192, 0, 2, 1, 0]))).toThrow(RangeError);
- });
- });
-
- describe("static fromString", () => {
- it("creates IPv4 from valid string", () => {
- const ip = IPv4.fromString("203.0.113.5");
- expect(ip.value).toBe(0xCB007105n);
- });
-
- it("throws RangeError for invalid IP string", () => {
- expect(() => IPv4.fromString("256.0.0.0")).toThrow(RangeError);
- expect(() => IPv4.fromString("10")).toThrow(RangeError);
- expect(() => IPv4.fromString("invalid.ip.string")).toThrow(RangeError);
- });
- });
-
- describe("binary", () => {
- it("returns correct 4-octet array", () => {
- const ip = IPv4.fromString("192.0.2.5");
- expect(Array.from(ip.binary())).toEqual([192, 0, 2, 5]);
- });
- });
-
- describe("[Symbol.toPrimitive]", () => {
- it("returns dotted-decimal string when hint is string", () => {
- const ip = IPv4.fromString("192.0.2.0");
- expect(`${ip}`).toBe("192.0.2.0");
- });
-
- it("returns number when hint is not string", () => {
- const ip = IPv4.fromString("192.0.2.0");
- expect(1n + (ip as unknown as bigint)).toBe(0xC0000201n);
- expect(BigInt(ip as unknown as bigint)).toBe(0xC0000200n);
- });
- });
-
- describe("toString", () => {
- it("returns dotted-decimal string", () => {
- const ip = IPv4.fromString("203.0.113.42");
- expect(ip.toString()).toBe("203.0.113.42");
- });
- });
-
- describe("equals", () => {
- it("returns true for two IPv4 instances with the same address", () => {
- const a = IPv4.fromString("198.51.100.10");
- const b = IPv4.fromString("198.51.100.10");
- expect(a.equals(b)).toBe(true);
- });
-
- it("returns false for two IPv4 instances with different addresses", () => {
- const a = IPv4.fromString("198.51.100.10");
- const b = IPv4.fromString("198.51.100.11");
- expect(a.equals(b)).toBe(false);
- });
-
- it("returns false when compared with a different IPAddress subclass", () => {
- const a = IPv4.fromString("198.51.100.10");
- const b = IPv6.fromString("::c633:640a");
-
- expect(a.equals(b)).toBe(false);
- });
- });
-
- describe("offset", () => {
- it("offsets positively", () => {
- const ip = IPv4.fromString("192.0.2.1");
- expect(ip.offset(1).toString()).toBe("192.0.2.2");
- expect(ip.offset(1n).toString()).toBe("192.0.2.2");
- expect(ip.offset(104030719).toString()).toBe("198.51.100.0");
- expect(ip.offset(104030719n).toString()).toBe("198.51.100.0");
- });
-
- it("offsets negatively", () => {
- const ip = IPv4.fromString("203.0.113.1");
- expect(ip.offset(-1).toString()).toBe("203.0.113.0");
- expect(ip.offset(-1n).toString()).toBe("203.0.113.0");
- expect(ip.offset(-80547073).toString()).toBe("198.51.100.0");
- expect(ip.offset(-80547073n).toString()).toBe("198.51.100.0");
- });
-
- it("throws TypeError if offset IP < 0", () => {
- const ip = IPv4.fromString("192.0.2.0");
- expect(() => ip.offset(-3221225985)).toThrow(TypeError);
- });
-
- it("throws TypeError if offset IP > 2^32 - 1", () => {
- const ip = IPv4.fromString("203.0.113.0");
- expect(() => ip.offset(889163520)).toThrow(TypeError);
- });
+ describe("static BIT_LENGTH", () => {
+ it("is 32", () => {
+ expect(IPv4.BIT_LENGTH).toBe(32);
});
+ });
+
+ describe("constructor", () => {
+ it("constructs from a valid number", () => {
+ const ip = new IPv4(0xC0000200);
+ expect(ip.value).toBe(0xC0000200n);
+ expect(ip.toString()).toBe("192.0.2.0");
+ });
+
+ it("constructs from a valid bigint", () => {
+ const ip = new IPv4(0xC6336410n);
+ expect(ip.value).toBe(0xC6336410n);
+ expect(ip.toString()).toBe("198.51.100.16");
+ });
+
+ it("constructs from 0", () => {
+ const ip = new IPv4(0);
+ expect(ip.value).toBe(0n);
+ expect(ip.toString()).toBe("0.0.0.0");
+ });
+
+ it("constructs from 2^32 - 1", () => {
+ const ip = new IPv4(2 ** 32 - 1);
+ expect(ip.value).toBe(2n ** 32n - 1n);
+ expect(ip.toString()).toBe("255.255.255.255");
+ });
+
+ it("throws TypeError for value < 0", () => {
+ expect(() => new IPv4(-1)).toThrow(TypeError);
+ expect(() => new IPv4(-1n)).toThrow(TypeError);
+ });
+
+ it("throws TypeError for value > 2^32 - 1", () => {
+ expect(() => new IPv4(2 ** 32)).toThrow(TypeError);
+ expect(() => new IPv4(2n ** 32n)).toThrow(TypeError);
+ });
+ });
+
+ describe("static fromBinary", () => {
+ it("creates IPv4 from 4-octet array", () => {
+ const ip = IPv4.fromBinary(new Uint8Array([192, 0, 2, 0]));
+ expect(ip.toString()).toBe("192.0.2.0");
+ });
+
+ it("throws RangeError if array has less than 4 octets", () => {
+ expect(() => IPv4.fromBinary(new Uint8Array([192, 0, 2]))).toThrow(
+ RangeError,
+ );
+ });
+
+ it("throws RangeError if array has more than 4 octets", () => {
+ expect(() => IPv4.fromBinary(new Uint8Array([192, 0, 2, 1, 0]))).toThrow(
+ RangeError,
+ );
+ });
+ });
+
+ describe("static fromString", () => {
+ it("creates IPv4 from valid string", () => {
+ const ip = IPv4.fromString("203.0.113.5");
+ expect(ip.value).toBe(0xCB007105n);
+ });
+
+ it("throws RangeError for invalid IP string", () => {
+ expect(() => IPv4.fromString("256.0.0.0")).toThrow(RangeError);
+ expect(() => IPv4.fromString("10")).toThrow(RangeError);
+ expect(() => IPv4.fromString("invalid.ip.string")).toThrow(RangeError);
+ });
+ });
+
+ describe("binary", () => {
+ it("returns correct 4-octet array", () => {
+ const ip = IPv4.fromString("192.0.2.5");
+ expect(Array.from(ip.binary())).toEqual([192, 0, 2, 5]);
+ });
+ });
+
+ describe("[Symbol.toPrimitive]", () => {
+ it("returns dotted-decimal string when hint is string", () => {
+ const ip = IPv4.fromString("192.0.2.0");
+ expect(`${ip}`).toBe("192.0.2.0");
+ });
+
+ it("returns number when hint is not string", () => {
+ const ip = IPv4.fromString("192.0.2.0");
+ expect(1n + (ip as unknown as bigint)).toBe(0xC0000201n);
+ expect(BigInt(ip as unknown as bigint)).toBe(0xC0000200n);
+ });
+ });
+
+ describe("toString", () => {
+ it("returns dotted-decimal string", () => {
+ const ip = IPv4.fromString("203.0.113.42");
+ expect(ip.toString()).toBe("203.0.113.42");
+ });
+ });
+
+ describe("equals", () => {
+ it("returns true for two IPv4 instances with the same address", () => {
+ const a = IPv4.fromString("198.51.100.10");
+ const b = IPv4.fromString("198.51.100.10");
+ expect(a.equals(b)).toBe(true);
+ });
+
+ it("returns false for two IPv4 instances with different addresses", () => {
+ const a = IPv4.fromString("198.51.100.10");
+ const b = IPv4.fromString("198.51.100.11");
+ expect(a.equals(b)).toBe(false);
+ });
+
+ it("returns false when compared with a different IPAddress subclass", () => {
+ const a = IPv4.fromString("198.51.100.10");
+ const b = IPv6.fromString("::c633:640a");
+
+ expect(a.equals(b)).toBe(false);
+ });
+ });
+
+ describe("offset", () => {
+ it("offsets positively", () => {
+ const ip = IPv4.fromString("192.0.2.1");
+ expect(ip.offset(1).toString()).toBe("192.0.2.2");
+ expect(ip.offset(1n).toString()).toBe("192.0.2.2");
+ expect(ip.offset(104030719).toString()).toBe("198.51.100.0");
+ expect(ip.offset(104030719n).toString()).toBe("198.51.100.0");
+ });
+
+ it("offsets negatively", () => {
+ const ip = IPv4.fromString("203.0.113.1");
+ expect(ip.offset(-1).toString()).toBe("203.0.113.0");
+ expect(ip.offset(-1n).toString()).toBe("203.0.113.0");
+ expect(ip.offset(-80547073).toString()).toBe("198.51.100.0");
+ expect(ip.offset(-80547073n).toString()).toBe("198.51.100.0");
+ });
+
+ it("throws TypeError if offset IP < 0", () => {
+ const ip = IPv4.fromString("192.0.2.0");
+ expect(() => ip.offset(-3221225985)).toThrow(TypeError);
+ });
+
+ it("throws TypeError if offset IP > 2^32 - 1", () => {
+ const ip = IPv4.fromString("203.0.113.0");
+ expect(() => ip.offset(889163520)).toThrow(TypeError);
+ });
+ });
});
diff --git a/tests/IPv6.test.ts b/tests/IPv6.test.ts
index 29c8a3f..db8c881 100644
--- a/tests/IPv6.test.ts
+++ b/tests/IPv6.test.ts
@@ -1,174 +1,215 @@
-import {describe, expect, it} from "vitest";
-import {IPv4, IPv6} from "../src/index.js";
+import { describe, expect, it } from "vitest";
+import { IPv4, IPv6 } from "../src/index.js";
describe("IPv6", () => {
- describe("static BIT_LENGTH", () => {
- it("is 128", () => {
- expect(IPv6.BIT_LENGTH).toBe(128);
- });
- });
-
- describe("constructor", () => {
- it("constructs from a valid 128-bit bigint", () => {
- const ip = new IPv6(0x20010db8000000000000000000000001n);
- expect(ip.value).toBe(0x20010db8000000000000000000000001n);
- expect(ip.toString()).toBe("2001:db8::1");
- });
-
- it("throws TypeError if value is negative", () => {
- expect(() => new IPv6(-1n)).toThrow(TypeError);
- });
-
- it("throws TypeError if value > 2^128 - 1", () => {
- expect(() => new IPv6(2n ** 128n)).toThrow(TypeError);
- });
- });
-
- describe("static fromBinary", () => {
- it("creates IPv6 from 8 hextets", () => {
- const ip = IPv6.fromBinary(new Uint16Array([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1]));
- expect(ip.toString()).toBe("2001:db8::1");
- });
-
- it("throws RangeError if array has less than 8 hextets", () => {
- expect(() => IPv6.fromBinary(new Uint16Array([0x2001, 0xdb8, 0, 0, 0, 0, 0x1]))).toThrow(RangeError);
- });
-
- it("throws RangeError if array has more than 8 hextets", () => {
- expect(() => IPv6.fromBinary(new Uint16Array([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0, 0x1])))
- .toThrow(RangeError);
- });
- });
-
- describe("static fromString", () => {
- it("creates IPv6 from valid compressed string", () => {
- const ip = IPv6.fromString("2001:db8::5");
- expect(ip.toString()).toBe("2001:db8::5");
- });
-
- it("creates IPv6 from valid full string", () => {
- const ip = IPv6.fromString("2001:0db8:0000:0000:0000:0000:0000:0001");
- expect(ip.toString()).toBe("2001:db8::1");
- });
-
- it("throws RangeError for invalid IPv6 string", () => {
- expect(() => IPv6.fromString("invalid::ip")).toThrow(RangeError);
- expect(() => IPv6.fromString("12345::")).toThrow(RangeError);
- expect(() => IPv6.fromString("2001:0db8:0000:0000:0000:0000:0000:ffff2")).toThrow(RangeError);
- expect(() => IPv6.fromString("2001:0db8:0000:0000:0000:0000:0001")).toThrow(RangeError);
- });
- });
-
- describe("binary", () => {
- it("returns correct 8-hextet array", () => {
- expect(Array.from(IPv6.fromString("2001:db8::").binary())).toEqual([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0]);
- expect(Array.from(IPv6.fromString("2001:db8:dead:beef::1337:cafe").binary()))
- .toEqual([0x2001, 0xdb8, 0xdead, 0xbeef, 0, 0, 0x1337, 0xcafe]);
- });
- });
-
- describe("hasMappedIPv4", () => {
- it("returns true for ::ffff:192.0.2.0", () => {
- const ip = IPv6.fromString("::ffff:192.0.2.0");
- expect(ip.hasMappedIPv4()).toBe(true);
- });
-
- it("returns true for ::ffff:c633:642a", () => {
- const ip = IPv6.fromString("::ffff:c633:642a");
- expect(ip.hasMappedIPv4()).toBe(true);
- });
-
- it("returns false for non-mapped IPv6 address", () => {
- const ip = IPv6.fromString("2001:db8::1");
- expect(ip.hasMappedIPv4()).toBe(false);
- });
- });
-
- describe("getMappedIPv4", () => {
- it("returns the mapped IPv4 address from ::ffff:192.0.2.0", () => {
- const ip = IPv6.fromString("::ffff:192.0.2.0");
- const mapped = ip.getMappedIPv4();
- expect(mapped.toString()).toBe("192.0.2.0");
- });
- it("returns the mapped IPv4 address from ::ffff:c633:642a", () => {
- const ip = IPv6.fromString("::ffff:c633:642a");
- const mapped = ip.getMappedIPv4();
- expect(mapped.toString()).toBe("198.51.100.42");
- });
- });
-
- describe("toString", () => {
- it("returns canonical string form", () => {
- const ip = IPv6.fromString("2001:0db8:0000:0000:0000:0000:0000:0001");
- expect(ip.toString()).toBe("2001:db8::1");
- });
-
- it("returns full uncompressed IPv6 string when no zeros to compress", () => {
- const ip = IPv6.fromString("2001:0db8:1234:5678:9abc:def:0123:4567");
- expect(ip.toString()).toBe("2001:db8:1234:5678:9abc:def:123:4567");
- });
-
- it("collapses the first longest zero group", () => {
- const hextets = new Uint16Array([0x2001, 0xdb8, 0, 0, 0x1, 0, 0, 0x1]);
- const ip = IPv6.fromBinary(hextets);
- expect(ip.toString()).toBe("2001:db8::1:0:0:1");
- });
-
- it("does not compress a single zero hextet", () => {
- const hextets = new Uint16Array([0x2001, 0xdb8, 0, 0x1, 0, 0x1234, 0xabcd, 0x5678]);
- const ip = IPv6.fromBinary(hextets);
- expect(ip.toString()).toBe("2001:db8:0:1:0:1234:abcd:5678");
- });
-
- it("returns :: for the unspecified address", () => {
- const ip = new IPv6(0n);
- expect(ip.toString()).toBe("::");
- });
- });
-
- describe("equals", () => {
- it("returns true for two equal IPv6 addresses", () => {
- const a = IPv6.fromString("2001:db8::5");
- const b = IPv6.fromString("2001:db8::5");
- expect(a.equals(b)).toBe(true);
- });
-
- it("returns false for different IPv6 addresses", () => {
- const a = IPv6.fromString("2001:db8::1");
- const b = IPv6.fromString("2001:db8::2");
- expect(a.equals(b)).toBe(false);
- });
-
- it("returns false when compared with different IPAddress subclass", () => {
- const a = IPv6.fromString("::cb00:71ff");
- const b = IPv4.fromString("203.0.113.255");
- expect(a.equals(b)).toBe(false);
- });
- });
-
- describe("offset", () => {
- it("offsets positively", () => {
- expect(IPv6.fromString("2001:db8::").offset(1).toString()).toBe("2001:db8::1");
- expect(IPv6.fromString("2001:db8::").offset(5634002667680789686395290983n).toString())
- .toBe("2001:db8:1234:5678:9abc:def:123:4567");
- });
-
- it("offsets negatively", () => {
- expect(IPv6.fromString("2001:db8::1").offset(-1).toString()).toBe("2001:db8::");
- expect(IPv6.fromString("2001:db8::cafe:babe").offset(-270593984n).toString())
- .toBe("2001:db8::badd:cafe");
- });
-
- it("throws TypeError if offset IP < 0", () => {
- expect(() => IPv6.fromString("::").offset(-1)).toThrow(TypeError);
- expect(() => IPv6.fromString("2001:db8::cafe:babe")
- .offset(-42540766411282592856903984955059518143n)).toThrow(TypeError);
- });
-
- it("throws TypeError if offset IP > 2^128 - 1", () => {
- expect(() => new IPv6(2n ** 128n - 1n).offset(1)).toThrow(TypeError);
- expect(() => IPv6.fromString("2001:db8::dead:beef")
- .offset(297741600509655870606470622476378456337n)).toThrow(TypeError);
- });
+ describe("static BIT_LENGTH", () => {
+ it("is 128", () => {
+ expect(IPv6.BIT_LENGTH).toBe(128);
});
+ });
+
+ describe("constructor", () => {
+ it("constructs from a valid 128-bit bigint", () => {
+ const ip = new IPv6(0x20010db8000000000000000000000001n);
+ expect(ip.value).toBe(0x20010db8000000000000000000000001n);
+ expect(ip.toString()).toBe("2001:db8::1");
+ });
+
+ it("throws TypeError if value is negative", () => {
+ expect(() => new IPv6(-1n)).toThrow(TypeError);
+ });
+
+ it("throws TypeError if value > 2^128 - 1", () => {
+ expect(() => new IPv6(2n ** 128n)).toThrow(TypeError);
+ });
+ });
+
+ describe("static fromBinary", () => {
+ it("creates IPv6 from 8 hextets", () => {
+ const ip = IPv6.fromBinary(
+ new Uint16Array([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1]),
+ );
+ expect(ip.toString()).toBe("2001:db8::1");
+ });
+
+ it("throws RangeError if array has less than 8 hextets", () => {
+ expect(() =>
+ IPv6.fromBinary(new Uint16Array([0x2001, 0xdb8, 0, 0, 0, 0, 0x1]))
+ ).toThrow(RangeError);
+ });
+
+ it("throws RangeError if array has more than 8 hextets", () => {
+ expect(() =>
+ IPv6.fromBinary(new Uint16Array([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0, 0x1]))
+ )
+ .toThrow(RangeError);
+ });
+ });
+
+ describe("static fromString", () => {
+ it("creates IPv6 from valid compressed string", () => {
+ const ip = IPv6.fromString("2001:db8::5");
+ expect(ip.toString()).toBe("2001:db8::5");
+ });
+
+ it("creates IPv6 from valid full string", () => {
+ const ip = IPv6.fromString("2001:0db8:0000:0000:0000:0000:0000:0001");
+ expect(ip.toString()).toBe("2001:db8::1");
+ });
+
+ it("throws RangeError for invalid IPv6 string", () => {
+ expect(() => IPv6.fromString("invalid::ip")).toThrow(RangeError);
+ expect(() => IPv6.fromString("12345::")).toThrow(RangeError);
+ expect(() => IPv6.fromString("2001:0db8:0000:0000:0000:0000:0000:ffff2"))
+ .toThrow(RangeError);
+ expect(() => IPv6.fromString("2001:0db8:0000:0000:0000:0000:0001"))
+ .toThrow(RangeError);
+ });
+ });
+
+ describe("binary", () => {
+ it("returns correct 8-hextet array", () => {
+ expect(Array.from(IPv6.fromString("2001:db8::").binary())).toEqual([
+ 0x2001,
+ 0xdb8,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ]);
+ expect(
+ Array.from(IPv6.fromString("2001:db8:dead:beef::1337:cafe").binary()),
+ )
+ .toEqual([0x2001, 0xdb8, 0xdead, 0xbeef, 0, 0, 0x1337, 0xcafe]);
+ });
+ });
+
+ describe("hasMappedIPv4", () => {
+ it("returns true for ::ffff:192.0.2.0", () => {
+ const ip = IPv6.fromString("::ffff:192.0.2.0");
+ expect(ip.hasMappedIPv4()).toBe(true);
+ });
+
+ it("returns true for ::ffff:c633:642a", () => {
+ const ip = IPv6.fromString("::ffff:c633:642a");
+ expect(ip.hasMappedIPv4()).toBe(true);
+ });
+
+ it("returns false for non-mapped IPv6 address", () => {
+ const ip = IPv6.fromString("2001:db8::1");
+ expect(ip.hasMappedIPv4()).toBe(false);
+ });
+ });
+
+ describe("getMappedIPv4", () => {
+ it("returns the mapped IPv4 address from ::ffff:192.0.2.0", () => {
+ const ip = IPv6.fromString("::ffff:192.0.2.0");
+ const mapped = ip.getMappedIPv4();
+ expect(mapped.toString()).toBe("192.0.2.0");
+ });
+ it("returns the mapped IPv4 address from ::ffff:c633:642a", () => {
+ const ip = IPv6.fromString("::ffff:c633:642a");
+ const mapped = ip.getMappedIPv4();
+ expect(mapped.toString()).toBe("198.51.100.42");
+ });
+ });
+
+ describe("toString", () => {
+ it("returns canonical string form", () => {
+ const ip = IPv6.fromString("2001:0db8:0000:0000:0000:0000:0000:0001");
+ expect(ip.toString()).toBe("2001:db8::1");
+ });
+
+ it("returns full uncompressed IPv6 string when no zeros to compress", () => {
+ const ip = IPv6.fromString("2001:0db8:1234:5678:9abc:def:0123:4567");
+ expect(ip.toString()).toBe("2001:db8:1234:5678:9abc:def:123:4567");
+ });
+
+ it("collapses the first longest zero group", () => {
+ const hextets = new Uint16Array([0x2001, 0xdb8, 0, 0, 0x1, 0, 0, 0x1]);
+ const ip = IPv6.fromBinary(hextets);
+ expect(ip.toString()).toBe("2001:db8::1:0:0:1");
+ });
+
+ it("does not compress a single zero hextet", () => {
+ const hextets = new Uint16Array([
+ 0x2001,
+ 0xdb8,
+ 0,
+ 0x1,
+ 0,
+ 0x1234,
+ 0xabcd,
+ 0x5678,
+ ]);
+ const ip = IPv6.fromBinary(hextets);
+ expect(ip.toString()).toBe("2001:db8:0:1:0:1234:abcd:5678");
+ });
+
+ it("returns :: for the unspecified address", () => {
+ const ip = new IPv6(0n);
+ expect(ip.toString()).toBe("::");
+ });
+ });
+
+ describe("equals", () => {
+ it("returns true for two equal IPv6 addresses", () => {
+ const a = IPv6.fromString("2001:db8::5");
+ const b = IPv6.fromString("2001:db8::5");
+ expect(a.equals(b)).toBe(true);
+ });
+
+ it("returns false for different IPv6 addresses", () => {
+ const a = IPv6.fromString("2001:db8::1");
+ const b = IPv6.fromString("2001:db8::2");
+ expect(a.equals(b)).toBe(false);
+ });
+
+ it("returns false when compared with different IPAddress subclass", () => {
+ const a = IPv6.fromString("::cb00:71ff");
+ const b = IPv4.fromString("203.0.113.255");
+ expect(a.equals(b)).toBe(false);
+ });
+ });
+
+ describe("offset", () => {
+ it("offsets positively", () => {
+ expect(IPv6.fromString("2001:db8::").offset(1).toString()).toBe(
+ "2001:db8::1",
+ );
+ expect(
+ IPv6.fromString("2001:db8::").offset(5634002667680789686395290983n)
+ .toString(),
+ )
+ .toBe("2001:db8:1234:5678:9abc:def:123:4567");
+ });
+
+ it("offsets negatively", () => {
+ expect(IPv6.fromString("2001:db8::1").offset(-1).toString()).toBe(
+ "2001:db8::",
+ );
+ expect(
+ IPv6.fromString("2001:db8::cafe:babe").offset(-270593984n).toString(),
+ )
+ .toBe("2001:db8::badd:cafe");
+ });
+
+ it("throws TypeError if offset IP < 0", () => {
+ expect(() => IPv6.fromString("::").offset(-1)).toThrow(TypeError);
+ expect(() =>
+ IPv6.fromString("2001:db8::cafe:babe")
+ .offset(-42540766411282592856903984955059518143n)
+ ).toThrow(TypeError);
+ });
+
+ it("throws TypeError if offset IP > 2^128 - 1", () => {
+ expect(() => new IPv6(2n ** 128n - 1n).offset(1)).toThrow(TypeError);
+ expect(() =>
+ IPv6.fromString("2001:db8::dead:beef")
+ .offset(297741600509655870606470622476378456337n)
+ ).toThrow(TypeError);
+ });
+ });
});
diff --git a/tests/Subnet.test.ts b/tests/Subnet.test.ts
index abeb00b..353d1ec 100644
--- a/tests/Subnet.test.ts
+++ b/tests/Subnet.test.ts
@@ -1,363 +1,392 @@
-import {describe, expect, it} from "vitest";
-import {IPv4, IPv6, Subnet} from "../src/index.js";
+import { describe, expect, it } from "vitest";
+import { IPv4, IPv6, Subnet } from "../src/index.js";
describe("Subnet", () => {
- describe("constructor", () => {
- it("creates a subnet with valid IPv4 address and prefix", () => {
- const addr = IPv4.fromString("192.0.2.0");
- const subnet = new Subnet(addr, 24);
- expect(subnet.address.equals(addr)).toBe(true);
- expect(subnet.prefix).toBe(24);
- });
-
- it("creates a subnet with misaligned address", () => {
- const addr = IPv4.fromString("192.0.2.42");
- const subnet = new Subnet(addr, 24);
- expect(subnet.address.equals(IPv4.fromString("192.0.2.0"))).toBe(true);
- });
-
- it("throws RangeError if prefix length exceeds IP bit length", () => {
- expect(() => new Subnet(IPv4.fromString("192.0.2.0"), 33)).toThrow(RangeError);
- expect(() => new Subnet(IPv6.fromString("2001:db8::"), 129)).toThrow(RangeError);
- });
-
- it("throws RangeError if prefix length is negative", () => {
- expect(() => new Subnet(IPv4.fromString("192.0.2.0"), -1)).toThrow(RangeError);
- });
- });
-
- describe("static fromCIDR", () => {
- it("creates subnet from valid IPv4 CIDR string", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
- expect(subnet.address.toString()).toBe("192.0.2.0");
- expect(subnet.prefix).toBe(24);
- });
-
- it("throws RangeError on invalid CIDR string", () => {
- expect(() => Subnet.fromCIDR("192.0.2.0/33")).toThrow(RangeError);
- expect(() => Subnet.fromCIDR("192.0.2.0")).toThrow(RangeError);
- expect(() => Subnet.fromCIDR("192.0.2.0/no")).toThrow(RangeError);
- expect(() => Subnet.fromCIDR("10/8")).toThrow(RangeError);
- expect(() => Subnet.fromCIDR("10.0/16")).toThrow(RangeError);
- expect(() => Subnet.fromCIDR("invalid/cidr")).toThrow(RangeError);
- });
- });
-
- describe("static range", () => {
- it("creates subnet from /24 IPv4 range", () => {
- const subnets = Subnet.range(
- IPv4.fromString("192.0.2.0"),
- IPv4.fromString("192.0.2.255")
- );
- expect(subnets.map(s => s.toString())).toEqual(["192.0.2.0/24"]);
- });
-
- it("creates subnet from misaligned IPv4 range", () => {
- const subnets = Subnet.range(
- IPv4.fromString("192.0.2.0"),
- IPv4.fromString("198.51.100.255")
- );
-
- // from https://account.arin.net/public/cidrCalculator
- expect(subnets.map(s => s.toString())).toEqual([
- "192.0.2.0/23",
- "192.0.4.0/22",
- "192.0.8.0/21",
- "192.0.16.0/20",
- "192.0.32.0/19",
- "192.0.64.0/18",
- "192.0.128.0/17",
- "192.1.0.0/16",
- "192.2.0.0/15",
- "192.4.0.0/14",
- "192.8.0.0/13",
- "192.16.0.0/12",
- "192.32.0.0/11",
- "192.64.0.0/10",
- "192.128.0.0/9",
- "193.0.0.0/8",
- "194.0.0.0/7",
- "196.0.0.0/7",
- "198.0.0.0/11",
- "198.32.0.0/12",
- "198.48.0.0/15",
- "198.50.0.0/16",
- "198.51.0.0/18",
- "198.51.64.0/19",
- "198.51.96.0/22",
- "198.51.100.0/24",
- ]);
- });
- });
-
- describe("netmask", () => {
- it("returns correct netmask for IPv4 /24", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
- expect(subnet.netmask()).toBe(0xffffff00n);
- });
- });
-
- describe("wildcard", () => {
- it("returns correct wildcard for IPv4 /24", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
- expect(subnet.wildcard()).toBe(0xffn);
- });
- });
-
- describe("size", () => {
- it("returns correct size for IPv4 /24 subnet", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
- expect(subnet.size()).toBe(256n);
- });
-
- it("returns correct size for IPv6 /32 subnet", () => {
- const subnet = Subnet.fromCIDR("2001:db8::/32");
- expect(subnet.size()).toBe(2n ** 96n);
- });
- });
-
- describe("contains", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
-
- it("contains address inside subnet", () => {
- const addr = IPv4.fromString("192.0.2.42");
- expect(subnet.contains(addr)).toBe(true);
- });
-
- it("does not contain address outside subnet", () => {
- const addr = IPv4.fromString("198.51.100.10");
- expect(subnet.contains(addr)).toBe(false);
- });
- });
-
- describe("containsSubnet", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
-
- it("contains smaller subnet inside", () => {
- const smaller = Subnet.fromCIDR("192.0.2.128/25");
- expect(subnet.containsSubnet(smaller)).toBe(true);
- });
-
- it("does not contain larger subnet", () => {
- const larger = Subnet.fromCIDR("192.0.2.0/23");
- expect(subnet.containsSubnet(larger)).toBe(false);
- });
-
- it("does not contain adjacent subnet", () => {
- const adjacent = Subnet.fromCIDR("192.0.3.0/25");
- expect(subnet.containsSubnet(adjacent)).toBe(false);
- });
- });
-
- describe("at", () => {
- const subnet = Subnet.fromCIDR("192.0.2.4/30");
-
- it("returns first address at index 0", () => {
- expect(subnet.at(0).toString()).toBe("192.0.2.4");
- });
-
- it("returns second address at index 1", () => {
- expect(subnet.at(1)?.toString()).toBe("192.0.2.5");
- });
-
- it("returns last address at index -1", () => {
- expect(subnet.at(-1).toString()).toBe("192.0.2.7");
- });
-
- it("returns penultimate address at index -2", () => {
- expect(subnet.at(-2)?.toString()).toBe("192.0.2.6");
- });
-
- it("returns undefined for out-of-range positive index", () => {
- expect(subnet.at(4)).toBeUndefined();
- });
-
- it("returns undefined for out-of-range negative index", () => {
- expect(subnet.at(-5)).toBeUndefined();
- });
+ describe("constructor", () => {
+ it("creates a subnet with valid IPv4 address and prefix", () => {
+ const addr = IPv4.fromString("192.0.2.0");
+ const subnet = new Subnet(addr, 24);
+ expect(subnet.address.equals(addr)).toBe(true);
+ expect(subnet.prefix).toBe(24);
});
- describe("addresses", () => {
- const subnet = Subnet.fromCIDR("192.0.2.8/30");
+ it("creates a subnet with misaligned address", () => {
+ const addr = IPv4.fromString("192.0.2.42");
+ const subnet = new Subnet(addr, 24);
+ expect(subnet.address.equals(IPv4.fromString("192.0.2.0"))).toBe(true);
+ });
+
+ it("throws RangeError if prefix length exceeds IP bit length", () => {
+ expect(() => new Subnet(IPv4.fromString("192.0.2.0"), 33)).toThrow(
+ RangeError,
+ );
+ expect(() => new Subnet(IPv6.fromString("2001:db8::"), 129)).toThrow(
+ RangeError,
+ );
+ });
- it("iterates over all addresses", () => {
- const ips = Array.from(subnet.addresses()).map(ip => ip.toString());
- expect(ips).toEqual(["192.0.2.8", "192.0.2.9", "192.0.2.10", "192.0.2.11"]);
- });
+ it("throws RangeError if prefix length is negative", () => {
+ expect(() => new Subnet(IPv4.fromString("192.0.2.0"), -1)).toThrow(
+ RangeError,
+ );
});
+ });
- describe("[Symbol.iterator]", () => {
- const subnet = Subnet.fromCIDR("198.51.100.40/30");
- it("iterator works with for...of", () => {
- const ips: string[] = [];
- for (const ip of subnet)
- ips.push(ip.toString());
- expect(ips).toEqual(["198.51.100.40", "198.51.100.41", "198.51.100.42", "198.51.100.43"]);
- });
- })
+ describe("static fromCIDR", () => {
+ it("creates subnet from valid IPv4 CIDR string", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+ expect(subnet.address.toString()).toBe("192.0.2.0");
+ expect(subnet.prefix).toBe(24);
+ });
- describe("set", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/30");
+ it("throws RangeError on invalid CIDR string", () => {
+ expect(() => Subnet.fromCIDR("192.0.2.0/33")).toThrow(RangeError);
+ expect(() => Subnet.fromCIDR("192.0.2.0")).toThrow(RangeError);
+ expect(() => Subnet.fromCIDR("192.0.2.0/no")).toThrow(RangeError);
+ expect(() => Subnet.fromCIDR("10/8")).toThrow(RangeError);
+ expect(() => Subnet.fromCIDR("10.0/16")).toThrow(RangeError);
+ expect(() => Subnet.fromCIDR("invalid/cidr")).toThrow(RangeError);
+ });
+ });
+
+ describe("static range", () => {
+ it("creates subnet from /24 IPv4 range", () => {
+ const subnets = Subnet.range(
+ IPv4.fromString("192.0.2.0"),
+ IPv4.fromString("192.0.2.255"),
+ );
+ expect(subnets.map((s) => s.toString())).toEqual(["192.0.2.0/24"]);
+ });
- it("creates a set of all addresses", () => {
- const set = subnet.set();
- expect(set.size).toBe(4);
- const arr = Array.from(set).map(ip => ip.toString());
- expect(arr).toEqual(["192.0.2.0", "192.0.2.1", "192.0.2.2", "192.0.2.3"]);
- });
+ it("creates subnet from misaligned IPv4 range", () => {
+ const subnets = Subnet.range(
+ IPv4.fromString("192.0.2.0"),
+ IPv4.fromString("198.51.100.255"),
+ );
+
+ // from https://account.arin.net/public/cidrCalculator
+ expect(subnets.map((s) => s.toString())).toEqual([
+ "192.0.2.0/23",
+ "192.0.4.0/22",
+ "192.0.8.0/21",
+ "192.0.16.0/20",
+ "192.0.32.0/19",
+ "192.0.64.0/18",
+ "192.0.128.0/17",
+ "192.1.0.0/16",
+ "192.2.0.0/15",
+ "192.4.0.0/14",
+ "192.8.0.0/13",
+ "192.16.0.0/12",
+ "192.32.0.0/11",
+ "192.64.0.0/10",
+ "192.128.0.0/9",
+ "193.0.0.0/8",
+ "194.0.0.0/7",
+ "196.0.0.0/7",
+ "198.0.0.0/11",
+ "198.32.0.0/12",
+ "198.48.0.0/15",
+ "198.50.0.0/16",
+ "198.51.0.0/18",
+ "198.51.64.0/19",
+ "198.51.96.0/22",
+ "198.51.100.0/24",
+ ]);
});
+ });
- describe("toString", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
+ describe("netmask", () => {
+ it("returns correct netmask for IPv4 /24", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+ expect(subnet.netmask()).toBe(0xffffff00n);
+ });
+ });
+
+ describe("wildcard", () => {
+ it("returns correct wildcard for IPv4 /24", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+ expect(subnet.wildcard()).toBe(0xffn);
+ });
+ });
+
+ describe("size", () => {
+ it("returns correct size for IPv4 /24 subnet", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+ expect(subnet.size()).toBe(256n);
+ });
- it("returns CIDR notation string", () => {
- expect(subnet.toString()).toBe("192.0.2.0/24");
- });
+ it("returns correct size for IPv6 /32 subnet", () => {
+ const subnet = Subnet.fromCIDR("2001:db8::/32");
+ expect(subnet.size()).toBe(2n ** 96n);
});
+ });
- describe("isAdjacent", () => {
- const subnet1 = Subnet.fromCIDR("192.0.2.0/25");
- const subnet2 = Subnet.fromCIDR("192.0.2.128/25");
- const subnet3 = Subnet.fromCIDR("198.51.100.0/25");
+ describe("contains", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
- it("returns true for adjacent subnets", () => {
- expect(subnet1.isAdjacent(subnet2)).toBe(true);
- });
+ it("contains address inside subnet", () => {
+ const addr = IPv4.fromString("192.0.2.42");
+ expect(subnet.contains(addr)).toBe(true);
+ });
+
+ it("does not contain address outside subnet", () => {
+ const addr = IPv4.fromString("198.51.100.10");
+ expect(subnet.contains(addr)).toBe(false);
+ });
+ });
+
+ describe("containsSubnet", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+
+ it("contains smaller subnet inside", () => {
+ const smaller = Subnet.fromCIDR("192.0.2.128/25");
+ expect(subnet.containsSubnet(smaller)).toBe(true);
+ });
+
+ it("does not contain larger subnet", () => {
+ const larger = Subnet.fromCIDR("192.0.2.0/23");
+ expect(subnet.containsSubnet(larger)).toBe(false);
+ });
+
+ it("does not contain adjacent subnet", () => {
+ const adjacent = Subnet.fromCIDR("192.0.3.0/25");
+ expect(subnet.containsSubnet(adjacent)).toBe(false);
+ });
+ });
- it("returns false for non-adjacent subnets", () => {
- expect(subnet1.isAdjacent(subnet3)).toBe(false);
- });
+ describe("at", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.4/30");
+
+ it("returns first address at index 0", () => {
+ expect(subnet.at(0).toString()).toBe("192.0.2.4");
+ });
+
+ it("returns second address at index 1", () => {
+ expect(subnet.at(1)?.toString()).toBe("192.0.2.5");
+ });
+
+ it("returns last address at index -1", () => {
+ expect(subnet.at(-1).toString()).toBe("192.0.2.7");
+ });
+
+ it("returns penultimate address at index -2", () => {
+ expect(subnet.at(-2)?.toString()).toBe("192.0.2.6");
+ });
+
+ it("returns undefined for out-of-range positive index", () => {
+ expect(subnet.at(4)).toBeUndefined();
+ });
+
+ it("returns undefined for out-of-range negative index", () => {
+ expect(subnet.at(-5)).toBeUndefined();
+ });
+ });
+
+ describe("addresses", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.8/30");
+
+ it("iterates over all addresses", () => {
+ const ips = Array.from(subnet.addresses()).map((ip) => ip.toString());
+ expect(ips).toEqual([
+ "192.0.2.8",
+ "192.0.2.9",
+ "192.0.2.10",
+ "192.0.2.11",
+ ]);
+ });
+ });
+
+ describe("[Symbol.iterator]", () => {
+ const subnet = Subnet.fromCIDR("198.51.100.40/30");
+ it("iterator works with for...of", () => {
+ const ips: string[] = [];
+ for (const ip of subnet) {
+ ips.push(ip.toString());
+ }
+ expect(ips).toEqual([
+ "198.51.100.40",
+ "198.51.100.41",
+ "198.51.100.42",
+ "198.51.100.43",
+ ]);
+ });
+ });
+
+ describe("set", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/30");
+
+ it("creates a set of all addresses", () => {
+ const set = subnet.set();
+ expect(set.size).toBe(4);
+ const arr = Array.from(set).map((ip) => ip.toString());
+ expect(arr).toEqual(["192.0.2.0", "192.0.2.1", "192.0.2.2", "192.0.2.3"]);
+ });
+ });
+
+ describe("toString", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+
+ it("returns CIDR notation string", () => {
+ expect(subnet.toString()).toBe("192.0.2.0/24");
+ });
+ });
+
+ describe("isAdjacent", () => {
+ const subnet1 = Subnet.fromCIDR("192.0.2.0/25");
+ const subnet2 = Subnet.fromCIDR("192.0.2.128/25");
+ const subnet3 = Subnet.fromCIDR("198.51.100.0/25");
+
+ it("returns true for adjacent subnets", () => {
+ expect(subnet1.isAdjacent(subnet2)).toBe(true);
+ });
+
+ it("returns false for non-adjacent subnets", () => {
+ expect(subnet1.isAdjacent(subnet3)).toBe(false);
+ });
+
+ it("throws TypeError for different families", () => {
+ const ipv6Subnet = Subnet.fromCIDR("2001:db8::/64");
+ expect(() => subnet1.isAdjacent(ipv6Subnet)).toThrow(TypeError);
+ });
+ });
+
+ describe("canMerge", () => {
+ const subnet1 = Subnet.fromCIDR("192.0.2.0/25");
+ const subnet2 = Subnet.fromCIDR("192.0.2.128/25");
+ const subnet3 = Subnet.fromCIDR("192.0.2.0/26");
+
+ it("returns true for adjacent subnets of same size", () => {
+ expect(subnet1.canMerge(subnet2)).toBe(true);
+ });
+
+ it("returns false for subnets with different prefix length", () => {
+ expect(subnet1.canMerge(subnet3)).toBe(false);
+ });
+
+ it("returns false for different families", () => {
+ const ipv6Subnet = Subnet.fromCIDR("2001:db8::/25");
+ expect(subnet1.canMerge(ipv6Subnet)).toBe(false);
+ });
+ });
+
+ describe("merge", () => {
+ const subnet1 = Subnet.fromCIDR("192.0.2.0/25");
+ const subnet2 = Subnet.fromCIDR("192.0.2.128/25");
+
+ it("merges adjacent subnets of same size", () => {
+ const merged = subnet1.merge(subnet2);
+ expect(merged.prefix).toBe(24);
+ expect(merged.address.toString()).toBe("192.0.2.0");
+ });
+
+ it("merges when this is the upper neighbour", () => {
+ const merged = subnet2.merge(subnet1);
+ expect(merged.prefix).toBe(24);
+ expect(merged.address.toString()).toBe("192.0.2.0");
+ });
+
+ it("throws TypeError for different families", () => {
+ const ipv6Subnet = Subnet.fromCIDR("2001:db8::/25");
+ expect(() => subnet1.merge(ipv6Subnet)).toThrow(TypeError);
+ });
+
+ it("throws TypeError for different sizes", () => {
+ const subnet3 = Subnet.fromCIDR("192.0.2.128/26");
+ expect(() => subnet1.merge(subnet3)).toThrow(TypeError);
+ });
+
+ it("throws RangeError for non-adjacent subnets", () => {
+ const subnet3 = Subnet.fromCIDR("198.51.100.0/25");
+ expect(() => subnet1.merge(subnet3)).toThrow(RangeError);
+ });
+ });
+
+ describe("split", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+
+ it("splits subnet into smaller subnets", () => {
+ const splits = subnet.split(26).map((s) => s.toString());
+ expect(splits).toEqual([
+ "192.0.2.0/26",
+ "192.0.2.64/26",
+ "192.0.2.128/26",
+ "192.0.2.192/26",
+ ]);
+ });
+
+ it("throws RangeError if prefix length is smaller than current", () => {
+ expect(() => subnet.split(16)).toThrow(RangeError);
+ });
+
+ it("throws RangeError if prefix length exceeds IP bit length", () => {
+ expect(() => subnet.split(33)).toThrow(RangeError);
+ });
+ });
+
+ describe("subtract", () => {
+ const subnet = Subnet.fromCIDR("192.0.2.0/24");
+
+ it("subtracts a subnet fully contained", () => {
+ const toSubtract = Subnet.fromCIDR("192.0.2.0/26");
+ const result = subnet.subtract(toSubtract).map((s) => s.toString());
+ expect(result).toEqual([
+ "192.0.2.64/26",
+ "192.0.2.128/25",
+ ]);
+ });
+
+ it("subtracts subnet not aligned with base address", () => {
+ const toSubtract = Subnet.fromCIDR("192.0.2.128/26");
+ const result = subnet.subtract(toSubtract).map((s) => s.toString());
+ expect(result).toEqual([
+ "192.0.2.0/25",
+ "192.0.2.192/26",
+ ]);
+ });
+
+ it("returns self when subtracting non-overlapping subnet", () => {
+ const toSubtract = Subnet.fromCIDR("198.51.100.0/25");
+ const result = subnet.subtract(toSubtract);
+ expect(result).toEqual([subnet]);
+ });
+
+ it("returns empty array when fully covered", () => {
+ expect(subnet.subtract(Subnet.fromCIDR("192.0.2.0/24"))).toHaveLength(0);
+ expect(subnet.subtract(Subnet.fromCIDR("0.0.0.0/0"))).toHaveLength(0);
+ });
+
+ it("throws TypeError for different family", () => {
+ const ipv6Subnet = Subnet.fromCIDR("2001:db8::/64");
+ expect(() => subnet.subtract(ipv6Subnet)).toThrow(TypeError);
+ });
+ });
+
+ describe("equals", () => {
+ it("returns true for identical subnets", () => {
+ expect(
+ Subnet.fromCIDR("192.0.2.0/24").equals(Subnet.fromCIDR("192.0.2.0/24")),
+ )
+ .toBe(true);
+ expect(
+ Subnet.fromCIDR("2001:db8::/64").equals(
+ Subnet.fromCIDR("2001:db8::/64"),
+ ),
+ )
+ .toBe(true);
+ });
- it("throws TypeError for different families", () => {
- const ipv6Subnet = Subnet.fromCIDR("2001:db8::/64");
- expect(() => subnet1.isAdjacent(ipv6Subnet)).toThrow(TypeError);
- });
- });
-
- describe("canMerge", () => {
- const subnet1 = Subnet.fromCIDR("192.0.2.0/25");
- const subnet2 = Subnet.fromCIDR("192.0.2.128/25");
- const subnet3 = Subnet.fromCIDR("192.0.2.0/26");
-
- it("returns true for adjacent subnets of same size", () => {
- expect(subnet1.canMerge(subnet2)).toBe(true);
- });
-
- it("returns false for subnets with different prefix length", () => {
- expect(subnet1.canMerge(subnet3)).toBe(false);
- });
-
- it("returns false for different families", () => {
- const ipv6Subnet = Subnet.fromCIDR("2001:db8::/25");
- expect(subnet1.canMerge(ipv6Subnet)).toBe(false);
- });
- });
-
- describe("merge", () => {
- const subnet1 = Subnet.fromCIDR("192.0.2.0/25");
- const subnet2 = Subnet.fromCIDR("192.0.2.128/25");
-
- it("merges adjacent subnets of same size", () => {
- const merged = subnet1.merge(subnet2);
- expect(merged.prefix).toBe(24);
- expect(merged.address.toString()).toBe("192.0.2.0");
- });
-
- it("merges when this is the upper neighbour", () => {
- const merged = subnet2.merge(subnet1);
- expect(merged.prefix).toBe(24);
- expect(merged.address.toString()).toBe("192.0.2.0");
- });
-
- it("throws TypeError for different families", () => {
- const ipv6Subnet = Subnet.fromCIDR("2001:db8::/25");
- expect(() => subnet1.merge(ipv6Subnet)).toThrow(TypeError);
- });
-
- it("throws TypeError for different sizes", () => {
- const subnet3 = Subnet.fromCIDR("192.0.2.128/26");
- expect(() => subnet1.merge(subnet3)).toThrow(TypeError);
- });
-
- it("throws RangeError for non-adjacent subnets", () => {
- const subnet3 = Subnet.fromCIDR("198.51.100.0/25");
- expect(() => subnet1.merge(subnet3)).toThrow(RangeError);
- });
- });
-
- describe("split", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
-
- it("splits subnet into smaller subnets", () => {
- const splits = subnet.split(26).map(s => s.toString());
- expect(splits).toEqual([
- "192.0.2.0/26",
- "192.0.2.64/26",
- "192.0.2.128/26",
- "192.0.2.192/26",
- ]);
- });
-
- it("throws RangeError if prefix length is smaller than current", () => {
- expect(() => subnet.split(16)).toThrow(RangeError);
- });
-
- it("throws RangeError if prefix length exceeds IP bit length", () => {
- expect(() => subnet.split(33)).toThrow(RangeError);
- });
- });
-
- describe("subtract", () => {
- const subnet = Subnet.fromCIDR("192.0.2.0/24");
-
- it("subtracts a subnet fully contained", () => {
- const toSubtract = Subnet.fromCIDR("192.0.2.0/26");
- const result = subnet.subtract(toSubtract).map(s => s.toString());
- expect(result).toEqual([
- "192.0.2.64/26",
- "192.0.2.128/25",
- ]);
- });
-
- it("subtracts subnet not aligned with base address", () => {
- const toSubtract = Subnet.fromCIDR("192.0.2.128/26");
- const result = subnet.subtract(toSubtract).map(s => s.toString());
- expect(result).toEqual([
- "192.0.2.0/25",
- "192.0.2.192/26",
- ]);
- });
-
- it("returns self when subtracting non-overlapping subnet", () => {
- const toSubtract = Subnet.fromCIDR("198.51.100.0/25");
- const result = subnet.subtract(toSubtract);
- expect(result).toEqual([subnet]);
- });
-
- it("returns empty array when fully covered", () => {
- expect(subnet.subtract(Subnet.fromCIDR("192.0.2.0/24"))).toHaveLength(0);
- expect(subnet.subtract(Subnet.fromCIDR("0.0.0.0/0"))).toHaveLength(0);
- });
-
- it("throws TypeError for different family", () => {
- const ipv6Subnet = Subnet.fromCIDR("2001:db8::/64");
- expect(() => subnet.subtract(ipv6Subnet)).toThrow(TypeError);
- });
- });
-
- describe("equals", () => {
- it("returns true for identical subnets", () => {
- expect(Subnet.fromCIDR("192.0.2.0/24").equals(Subnet.fromCIDR("192.0.2.0/24")))
- .toBe(true);
- expect(Subnet.fromCIDR("2001:db8::/64").equals(Subnet.fromCIDR("2001:db8::/64")))
- .toBe(true);
- });
-
- it("returns false for different subnets", () => {
- expect(Subnet.fromCIDR("192.0.2.0/24").equals(Subnet.fromCIDR("192.0.2.0/25")))
- .toBe(false);
- expect(Subnet.fromCIDR("192.0.2.0/24").equals(Subnet.fromCIDR("2001:db8::/64")))
- .toBe(false);
- });
+ it("returns false for different subnets", () => {
+ expect(
+ Subnet.fromCIDR("192.0.2.0/24").equals(Subnet.fromCIDR("192.0.2.0/25")),
+ )
+ .toBe(false);
+ expect(
+ Subnet.fromCIDR("192.0.2.0/24").equals(
+ Subnet.fromCIDR("2001:db8::/64"),
+ ),
+ )
+ .toBe(false);
});
+ });
});
diff --git a/tests/SubnetList.test.ts b/tests/SubnetList.test.ts
index 8c7dcfb..011e482 100644
--- a/tests/SubnetList.test.ts
+++ b/tests/SubnetList.test.ts
@@ -1,239 +1,241 @@
-import {describe, expect, it} from "vitest";
-import {IPv4, IPv6, Subnet, SubnetList} from "../src/index.js";
+import { describe, expect, it } from "vitest";
+import { IPv4, IPv6, Subnet, SubnetList } from "../src/index.js";
describe("SubnetList", () => {
- describe("constructor", () => {
- it("creates empty SubnetList", () => {
- const list = new SubnetList();
- expect(list.size()).toBe(0n);
- expect(list.subnets()).toHaveLength(0);
- });
-
- it("initialises with provided subnets", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const s3 = Subnet.fromCIDR("198.51.100.0/24");
- const list = new SubnetList([s2, s1, s3]);
- expect(list.hasSubnet(s1)).toBe(true);
- expect(list.hasSubnet(s2)).toBe(true);
- expect(list.hasSubnet(s3)).toBe(true);
- expect(list.size()).toBeLessThanOrEqual(s1.size() + s2.size() + s3.size());
- });
-
- it("optimises subnets to minimise memory usage", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const s3 = Subnet.fromCIDR("198.51.100.0/24");
- const list = new SubnetList([s1, s2, s3]);
- expect(list.subnets().map(s => s.toString())).toEqual([
- "192.0.2.0/24",
- "198.51.100.0/24",
- ]);
- })
- });
-
- describe("add", () => {
- it("adds a subnet and returns true if new", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const s3 = Subnet.fromCIDR("198.51.100.0/24");
- const list = new SubnetList([s1, s2]);
- const initialSize = list.size();
- expect(list.add(s3)).toBe(true);
- expect(list.hasSubnet(s3)).toBe(true);
- expect(list.size()).toBe(initialSize + s3.size());
- });
-
- it("adds a subnet that expands an existing one and returns true", () => {
- const s1 = Subnet.fromCIDR("192.0.2.128/25");
- const s2 = Subnet.fromCIDR("192.0.2.0/24");
- const list = new SubnetList([s1]);
- expect(list.add(s2)).toBe(true);
- expect(list.hasSubnet(s2)).toBe(true);
- expect(list.size()).toBe(s2.size());
- });
-
- it("returns false when adding a duplicate subnet", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const list = new SubnetList([s1]);
- expect(list.add(s1)).toBe(false);
- });
-
- it("returns false when adding a subnet that is already contained", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/24");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const list = new SubnetList([s1]);
- expect(list.add(s2)).toBe(false);
- });
-
- it("adds an IP address as a /32 subnet", () => {
- const ip = IPv4.fromString("192.0.2.5");
- const list = new SubnetList();
- expect(list.add(ip)).toBe(true);
- expect(list.subnets().map(s => s.toString())).toEqual(["192.0.2.5/32"]);
- expect(list.contains(ip)).toBe(true);
- });
-
- it("returns false when adding duplicate IP address", () => {
- const ip = IPv4.fromString("192.0.2.5");
- const list = new SubnetList([Subnet.fromCIDR("192.0.2.5/32")]);
- expect(list.add(ip)).toBe(false);
- });
-
- it("adds all subnets from another SubnetList", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/24");
- const s2 = Subnet.fromCIDR("198.51.100.0/24");
- const other = new SubnetList([s1, s2]);
- const list = new SubnetList();
- list.add(other);
- expect(list.hasSubnet(s1)).toBe(true);
- expect(list.hasSubnet(s2)).toBe(true);
- expect(list.size()).toBeLessThanOrEqual(s1.size() + s2.size());
- });
- });
-
- describe("remove", () => {
- it("removes existing subnet and returns true", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const list = new SubnetList([s1]);
- expect(list.remove(s1)).toBe(true);
- expect(list.hasSubnet(s1)).toBe(false);
- expect(list.size()).toBe(0n);
- });
-
- it("removes subnet partially covered by a larger one", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const s3 = Subnet.fromCIDR("192.0.2.0/24");
- const list = new SubnetList([s3]);
- expect(list.remove(s2)).toBe(true);
- expect(list.hasSubnet(s1)).toBe(true);
- expect(list.hasSubnet(s2)).toBe(false);
- expect(list.size()).toBe(s1.size());
- });
-
- it("returns false when removing subnet not present", () => {
- const s1 = Subnet.fromCIDR("203.0.113.0/24");
- const list = new SubnetList([Subnet.fromCIDR("192.0.2.0/25")]);
- expect(list.remove(s1)).toBe(false);
- });
- });
-
- describe("hasSubnet", () => {
- it("returns true if subnet is in the list", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const list = new SubnetList([s1]);
- expect(list.hasSubnet(s1)).toBe(true);
- });
-
- it("returns true if subnet is part of a larger one contained in the list", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/24");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const list = new SubnetList([s1]);
- expect(list.hasSubnet(s2)).toBe(true);
- });
-
- it("returns false if subnet is not in the list", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const list = new SubnetList([Subnet.fromCIDR("198.51.100.0/24")]);
- expect(list.hasSubnet(s1)).toBe(false);
- });
- });
-
- describe("subnets", () => {
- it("returns all subnets", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const list = new SubnetList([s1, s2]);
- const all = list.subnets();
- for (const s of [s1, s2]) {
- const found = all.some(x => x.containsSubnet(s));
- expect(found).toBe(true);
- }
- const total = all.reduce((sum, s) => sum + s.size(), 0n);
- expect(total).toBe(s1.size() + s2.size());
- });
- });
-
- describe("contains", () => {
- it("returns true for IPs within any subnet", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/24");
- const s2 = Subnet.fromCIDR("198.51.100.0/24");
- const list = new SubnetList([s1, s2]);
- expect(list.contains(IPv4.fromString("192.0.2.78"))).toBe(true);
- expect(list.contains(IPv4.fromString("198.51.100.69"))).toBe(true);
- });
-
- it("returns false for IPs outside all subnets", () => {
- const ip = IPv4.fromString("198.51.100.1");
- const s = Subnet.fromCIDR("192.0.2.0/24");
- const list = new SubnetList([s]);
- expect(list.contains(ip)).toBe(false);
- });
- });
-
- describe("size", () => {
- it("returns sum of all subnet sizes", () => {
- const s1 = Subnet.fromCIDR("192.0.2.0/25");
- const s2 = Subnet.fromCIDR("192.0.2.128/25");
- const s3 = Subnet.fromCIDR("203.0.113.0/24");
- const list = new SubnetList([s1, s2, s3]);
- expect(list.size()).toBe(s1.size() + s2.size() + s3.size());
- });
-
- it("returns 0 when empty", () => {
- const list = new SubnetList();
- expect(list.size()).toBe(0n);
- });
- });
-
- describe("[Symbol.iterator]", () => {
- it("iterates over all IPs in all subnets", () => {
- const s1 = Subnet.fromCIDR("192.0.2.64/30");
- const s2 = Subnet.fromCIDR("192.0.2.24/30");
- const list = new SubnetList([s1, s2]);
- const ips = Array.from(list).map(ip => ip.toString());
- expect(ips).toEqual([
- "192.0.2.24",
- "192.0.2.25",
- "192.0.2.26",
- "192.0.2.27",
- "192.0.2.64",
- "192.0.2.65",
- "192.0.2.66",
- "192.0.2.67",
- ]);
- });
-
- it("iterates zero times when empty", () => {
- const list = new SubnetList();
- expect(Array.from(list)).toHaveLength(0);
- });
- });
-
- describe("static BOGON", () => {
- it("is a SubnetList instance", () => {
- expect(SubnetList.BOGON).toBeInstanceOf(SubnetList);
- });
-
- it("contains a common IPv4 bogon address", () => {
- const ip = IPv4.fromString("192.0.2.42");
- expect(SubnetList.BOGON.contains(ip)).toBe(true);
- });
-
- it("contains a common IPv6 bogon address", () => {
- const ip = IPv6.fromString("2001:db8::cafe:babe");
- expect(SubnetList.BOGON.contains(ip)).toBe(true);
- });
-
- it("does not contain a public IPv4 address", () => {
- const ip = IPv4.fromString("1.1.1.1");
- expect(SubnetList.BOGON.contains(ip)).toBe(false);
- });
-
- it("does not contain a public IPv6 address", () => {
- const ip = IPv6.fromString("2606:4700:4700::1111");
- expect(SubnetList.BOGON.contains(ip)).toBe(false);
- });
+ describe("constructor", () => {
+ it("creates empty SubnetList", () => {
+ const list = new SubnetList();
+ expect(list.size()).toBe(0n);
+ expect(list.subnets()).toHaveLength(0);
});
+
+ it("initialises with provided subnets", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const s3 = Subnet.fromCIDR("198.51.100.0/24");
+ const list = new SubnetList([s2, s1, s3]);
+ expect(list.hasSubnet(s1)).toBe(true);
+ expect(list.hasSubnet(s2)).toBe(true);
+ expect(list.hasSubnet(s3)).toBe(true);
+ expect(list.size()).toBeLessThanOrEqual(
+ s1.size() + s2.size() + s3.size(),
+ );
+ });
+
+ it("optimises subnets to minimise memory usage", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const s3 = Subnet.fromCIDR("198.51.100.0/24");
+ const list = new SubnetList([s1, s2, s3]);
+ expect(list.subnets().map((s) => s.toString())).toEqual([
+ "192.0.2.0/24",
+ "198.51.100.0/24",
+ ]);
+ });
+ });
+
+ describe("add", () => {
+ it("adds a subnet and returns true if new", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const s3 = Subnet.fromCIDR("198.51.100.0/24");
+ const list = new SubnetList([s1, s2]);
+ const initialSize = list.size();
+ expect(list.add(s3)).toBe(true);
+ expect(list.hasSubnet(s3)).toBe(true);
+ expect(list.size()).toBe(initialSize + s3.size());
+ });
+
+ it("adds a subnet that expands an existing one and returns true", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.128/25");
+ const s2 = Subnet.fromCIDR("192.0.2.0/24");
+ const list = new SubnetList([s1]);
+ expect(list.add(s2)).toBe(true);
+ expect(list.hasSubnet(s2)).toBe(true);
+ expect(list.size()).toBe(s2.size());
+ });
+
+ it("returns false when adding a duplicate subnet", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const list = new SubnetList([s1]);
+ expect(list.add(s1)).toBe(false);
+ });
+
+ it("returns false when adding a subnet that is already contained", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/24");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const list = new SubnetList([s1]);
+ expect(list.add(s2)).toBe(false);
+ });
+
+ it("adds an IP address as a /32 subnet", () => {
+ const ip = IPv4.fromString("192.0.2.5");
+ const list = new SubnetList();
+ expect(list.add(ip)).toBe(true);
+ expect(list.subnets().map((s) => s.toString())).toEqual(["192.0.2.5/32"]);
+ expect(list.contains(ip)).toBe(true);
+ });
+
+ it("returns false when adding duplicate IP address", () => {
+ const ip = IPv4.fromString("192.0.2.5");
+ const list = new SubnetList([Subnet.fromCIDR("192.0.2.5/32")]);
+ expect(list.add(ip)).toBe(false);
+ });
+
+ it("adds all subnets from another SubnetList", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/24");
+ const s2 = Subnet.fromCIDR("198.51.100.0/24");
+ const other = new SubnetList([s1, s2]);
+ const list = new SubnetList();
+ list.add(other);
+ expect(list.hasSubnet(s1)).toBe(true);
+ expect(list.hasSubnet(s2)).toBe(true);
+ expect(list.size()).toBeLessThanOrEqual(s1.size() + s2.size());
+ });
+ });
+
+ describe("remove", () => {
+ it("removes existing subnet and returns true", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const list = new SubnetList([s1]);
+ expect(list.remove(s1)).toBe(true);
+ expect(list.hasSubnet(s1)).toBe(false);
+ expect(list.size()).toBe(0n);
+ });
+
+ it("removes subnet partially covered by a larger one", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const s3 = Subnet.fromCIDR("192.0.2.0/24");
+ const list = new SubnetList([s3]);
+ expect(list.remove(s2)).toBe(true);
+ expect(list.hasSubnet(s1)).toBe(true);
+ expect(list.hasSubnet(s2)).toBe(false);
+ expect(list.size()).toBe(s1.size());
+ });
+
+ it("returns false when removing subnet not present", () => {
+ const s1 = Subnet.fromCIDR("203.0.113.0/24");
+ const list = new SubnetList([Subnet.fromCIDR("192.0.2.0/25")]);
+ expect(list.remove(s1)).toBe(false);
+ });
+ });
+
+ describe("hasSubnet", () => {
+ it("returns true if subnet is in the list", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const list = new SubnetList([s1]);
+ expect(list.hasSubnet(s1)).toBe(true);
+ });
+
+ it("returns true if subnet is part of a larger one contained in the list", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/24");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const list = new SubnetList([s1]);
+ expect(list.hasSubnet(s2)).toBe(true);
+ });
+
+ it("returns false if subnet is not in the list", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const list = new SubnetList([Subnet.fromCIDR("198.51.100.0/24")]);
+ expect(list.hasSubnet(s1)).toBe(false);
+ });
+ });
+
+ describe("subnets", () => {
+ it("returns all subnets", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const list = new SubnetList([s1, s2]);
+ const all = list.subnets();
+ for (const s of [s1, s2]) {
+ const found = all.some((x) => x.containsSubnet(s));
+ expect(found).toBe(true);
+ }
+ const total = all.reduce((sum, s) => sum + s.size(), 0n);
+ expect(total).toBe(s1.size() + s2.size());
+ });
+ });
+
+ describe("contains", () => {
+ it("returns true for IPs within any subnet", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/24");
+ const s2 = Subnet.fromCIDR("198.51.100.0/24");
+ const list = new SubnetList([s1, s2]);
+ expect(list.contains(IPv4.fromString("192.0.2.78"))).toBe(true);
+ expect(list.contains(IPv4.fromString("198.51.100.69"))).toBe(true);
+ });
+
+ it("returns false for IPs outside all subnets", () => {
+ const ip = IPv4.fromString("198.51.100.1");
+ const s = Subnet.fromCIDR("192.0.2.0/24");
+ const list = new SubnetList([s]);
+ expect(list.contains(ip)).toBe(false);
+ });
+ });
+
+ describe("size", () => {
+ it("returns sum of all subnet sizes", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.0/25");
+ const s2 = Subnet.fromCIDR("192.0.2.128/25");
+ const s3 = Subnet.fromCIDR("203.0.113.0/24");
+ const list = new SubnetList([s1, s2, s3]);
+ expect(list.size()).toBe(s1.size() + s2.size() + s3.size());
+ });
+
+ it("returns 0 when empty", () => {
+ const list = new SubnetList();
+ expect(list.size()).toBe(0n);
+ });
+ });
+
+ describe("[Symbol.iterator]", () => {
+ it("iterates over all IPs in all subnets", () => {
+ const s1 = Subnet.fromCIDR("192.0.2.64/30");
+ const s2 = Subnet.fromCIDR("192.0.2.24/30");
+ const list = new SubnetList([s1, s2]);
+ const ips = Array.from(list).map((ip) => ip.toString());
+ expect(ips).toEqual([
+ "192.0.2.24",
+ "192.0.2.25",
+ "192.0.2.26",
+ "192.0.2.27",
+ "192.0.2.64",
+ "192.0.2.65",
+ "192.0.2.66",
+ "192.0.2.67",
+ ]);
+ });
+
+ it("iterates zero times when empty", () => {
+ const list = new SubnetList();
+ expect(Array.from(list)).toHaveLength(0);
+ });
+ });
+
+ describe("static BOGON", () => {
+ it("is a SubnetList instance", () => {
+ expect(SubnetList.BOGON).toBeInstanceOf(SubnetList);
+ });
+
+ it("contains a common IPv4 bogon address", () => {
+ const ip = IPv4.fromString("192.0.2.42");
+ expect(SubnetList.BOGON.contains(ip)).toBe(true);
+ });
+
+ it("contains a common IPv6 bogon address", () => {
+ const ip = IPv6.fromString("2001:db8::cafe:babe");
+ expect(SubnetList.BOGON.contains(ip)).toBe(true);
+ });
+
+ it("does not contain a public IPv4 address", () => {
+ const ip = IPv4.fromString("1.1.1.1");
+ expect(SubnetList.BOGON.contains(ip)).toBe(false);
+ });
+
+ it("does not contain a public IPv6 address", () => {
+ const ip = IPv6.fromString("2606:4700:4700::1111");
+ expect(SubnetList.BOGON.contains(ip)).toBe(false);
+ });
+ });
});
diff --git a/vite.config.ts b/vite.config.ts
index 40d8bb8..c944b49 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,16 +1,16 @@
-import {defineConfig} from "vitest/config";
+import { defineConfig } from "vitest/config";
-declare const process: {env: Record}
+declare const process: { env: Record };
export default defineConfig({
- test: {
- reporters: process.env.GITHUB_ACTIONS ? "default" : ["default", "html"],
- coverage: {
- reporter: process.env.GITHUB_ACTIONS ? "text" : ["text", "html"],
- include: ["src/**"],
- thresholds: {
- 100: true,
- },
- },
+ test: {
+ reporters: process.env.GITHUB_ACTIONS ? "default" : ["default", "html"],
+ coverage: {
+ reporter: process.env.GITHUB_ACTIONS ? "text" : ["text", "html"],
+ include: ["src/**"],
+ thresholds: {
+ 100: true,
+ },
},
+ },
});