1import { assert, describe, it } from "@effect/vitest"
2import { assertTrue } from "@effect/vitest/utils"
3import { Equal, Hash, Result, Schema } from "effect"
4import * as IpInterface from "effect/net/IpInterface"
5import * as IpNetwork from "effect/net/IpNetwork"
6import * as NetAddress from "effect/net/NetAddress"
7import * as fc from "fast-check"
8import { inspect } from "node:util"
9
10const success = <A>(result: Result.Result<A, unknown>): A => {
11 assertTrue(Result.isSuccess(result), "expected Success")
12 return result.success
13}
14
15const failure = <E>(result: Result.Result<unknown, E>): E => {
16 assertTrue(Result.isFailure(result), "expected Failure")
17 return result.failure
18}
19
20const ip = (input: string): NetAddress.IpAddress => success(NetAddress.ipFromString(input))
21const network = (input: string): IpNetwork.IpNetwork => success(IpNetwork.fromString(input))
22
23describe("IpNetwork", () => {
24 it("serializes and inspects canonical strings", () => {
25 for (const expected of ["192.0.2.0/24", "2001:db8::/32"]) {
26 const address = IpNetwork.fromStringUnsafe(expected)
27 assert.strictEqual(address.toJSON(), expected)
28 assert.strictEqual(JSON.stringify(address), JSON.stringify(expected))
29 assert.strictEqual(inspect(address), expected)
30 }
31 })
32
33 it("retains the address supplied to checked constructors", () => {
34 const address = NetAddress.ipv4Loopback
35 assert.strictEqual(failure(IpNetwork.make(address, 33)).input, address)
36 assert.strictEqual(failure(IpNetwork.make(address, 8)).input, address)
37 assert.strictEqual(failure(IpNetwork.fromAddress(address, 33)).input, address)
38 })
39
40 it("forwards errors from address and prefix validation", () => {
41 for (const parse of [IpNetwork.fromString, IpNetwork.ipv4FromString]) {
42 assert.strictEqual(failure(parse("1.2.3.4/+24")).input, "1.2.3.4/+24")
43 assert.strictEqual(failure(parse("010.2.3.4/24")).input, "010.2.3.4")
44 assert.deepStrictEqual(failure(parse("1.2.3.4/33")).input, ip("1.2.3.4"))
45 }
46 for (const parse of [IpNetwork.fromString, IpNetwork.ipv6FromString]) {
47 assert.strictEqual(failure(parse("::ffff:192.000.2.1/128")).input, "192.000.2.1")
48 }
49 })
50
51 it("constructs immutable canonical networks and preserves family identity", () => {
52 const ipv4 = IpNetwork.makeUnsafe(NetAddress.ipv4Unspecified, 0)
53 const ipv6 = IpNetwork.makeUnsafe(NetAddress.ipv6Unspecified, 0)
54 assert.isTrue(Object.isFrozen(ipv4))
55 assert.isTrue(Object.isFrozen(ipv6))
56 assert.isTrue(IpNetwork.isIpv4Network(ipv4))
57 assert.isTrue(IpNetwork.isIpv6Network(ipv6))
58 assert.strictEqual(ipv4._tag, "IpNetwork")
59 assert.strictEqual(ipv6._tag, "IpNetwork")
60 assert.isFalse(IpNetwork.isIpv4Network(ipv6))
61 assert.isFalse(IpNetwork.isIpv6Network(ipv4))
62 assert.isTrue(IpNetwork.isIpNetwork(ipv4))
63 assert.isFalse(Equal.equals(ipv4, ipv6))
64 assert.strictEqual(ipv4.toString(), "0.0.0.0/0")
65 assert.strictEqual(ipv6.toString(), "::/0")
66 })
67
68 it("rejects invalid prefix lengths and host bits", () => {
69 const ipv4 = ip("10.1.2.3")
70 const ipv6 = ip("2001:db8::1")
71 for (const prefix of [1.5, Number.NaN, Number.POSITIVE_INFINITY, 33]) {
72 failure(IpNetwork.make(ipv4, prefix))
73 failure(IpNetwork.fromAddress(ipv4, prefix))
74 }
75 failure(IpNetwork.make(ipv6, 129))
76 assert.deepStrictEqual(failure(IpNetwork.fromString("10.1.2.3/8")).input, ipv4)
77 failure(IpNetwork.make(ipv4, 8))
78 failure(IpNetwork.make(ipv6, 32))
79
80 const error = failure(IpNetwork.make(ipv4, -1))
81 assert.strictEqual(error._tag, "NetAddressError")
82 assert.strictEqual(error.message, "prefix length must be an integer from 0 through 32")
83 })
84
85 it("explicitly truncates byte-aligned and partial-byte host addresses", () => {
86 assert.strictEqual(IpNetwork.format(success(IpNetwork.fromAddress(ip("10.1.2.3"), 8))), "10.0.0.0/8")
87 assert.strictEqual(IpNetwork.format(success(IpNetwork.fromAddress(ip("192.0.2.255"), 25))), "192.0.2.128/25")
88 assert.strictEqual(
89 IpNetwork.format(success(IpNetwork.fromAddress(ip("2001:db8:0:1:ffff::1"), 63))),
90 "2001:db8::/63"
91 )
92 assert.strictEqual(
93 IpNetwork.format(success(IpNetwork.fromAddress(ip("2001:db8:0:1:ffff::1"), 65))),
94 "2001:db8:0:1:8000::/65"
95 )
96 })
97
98 it("requires derived addresses to revalidate refinements", () => {
99 const multicast = NetAddress.ipv4FromBytesUnsafe(new Uint8Array([224, 0, 0, 1]))
100 if (!NetAddress.isMulticast(multicast)) throw new Error("expected multicast test address")
101 const direct = IpNetwork.fromAddressUnsafe(multicast, 0)
102 const viaInterface = IpNetwork.fromInterface(IpInterface.makeUnsafe(multicast, 0))
103 for (const derived of [direct, viaInterface]) {
104 assert.strictEqual(NetAddress.formatIp(derived.address), "0.0.0.0")
105 assert.isFalse(NetAddress.isMulticast(derived.address))
106 const last = IpNetwork.lastAddress(derived)
107 assert.strictEqual(NetAddress.formatIp(last), "255.255.255.255")
108 assert.isFalse(NetAddress.isMulticast(last))
109 }
110
111 const unspecifiedNetwork = IpNetwork.makeUnsafe(NetAddress.ipv4Unspecified, 0)
112 const broadcast = IpNetwork.lastAddress(unspecifiedNetwork)
113 assert.isFalse(NetAddress.isUnspecified(broadcast))
114 assert.isTrue(NetAddress.isBroadcast(broadcast))
115
116 const loopback = ip("::1")
117 assert.isTrue(NetAddress.isLoopback(loopback))
118 if (NetAddress.isLoopback(loopback)) {
119 const directLoopback = IpNetwork.fromAddressUnsafe(loopback, 64)
120 const interfaceLoopback = IpNetwork.fromInterface(IpInterface.makeUnsafe(loopback, 64))
121 for (const derived of [directLoopback, interfaceLoopback]) {
122 assert.strictEqual(NetAddress.formatIp(derived.address), "::")
123 assert.isFalse(NetAddress.isLoopback(derived.address))
124 assert.isTrue(NetAddress.isUnspecified(derived.address))
125 }
126 }
127 })
128
129 it("parses strict CIDR and formats canonical address text", () => {
130 assert.strictEqual(IpNetwork.format(network("192.0.2.0/24")), "192.0.2.0/24")
131 assert.strictEqual(
132 IpNetwork.format(network("2001:0DB8:0000:0000:0000:0000:0000:0000/32")),
133 "2001:db8::/32"
134 )
135 assert.strictEqual(IpNetwork.format(network("::ffff:192.0.2.0/120")), "::ffff:192.0.2.0/120")
136 })
137
138 it("rejects malformed and non-network CIDR input", () => {
139 for (
140 const input of [
141 "1.2.3.4",
142 "1.2.3.4/",
143 "/24",
144 "1.2.3.4/24/1",
145 "1.2.3.4/+24",
146 "1.2.3.4/024",
147 "1.2.3.4/24 ",
148 "1.2.3.4/1e1",
149 "1.2.3.4/33",
150 "10.1.2.3/8",
151 "2001:db8::1/32"
152 ]
153 ) {
154 failure(IpNetwork.fromString(input))
155 }
156 const parseError = failure(IpNetwork.fromString("1.2.3.4/+24"))
157 assert.strictEqual(parseError.message, "prefix length must be an unpadded ASCII decimal integer")
158 failure(IpNetwork.ipv4FromString("::/0"))
159 failure(IpNetwork.ipv6FromString("0.0.0.0/0"))
160 })
161
162 it("uses numeric equality and matching hashes", () => {
163 const first = network("2001:0DB8:0:0:0:0:0:0/32")
164 const second = network("2001:db8::/32")
165 assert.isTrue(Equal.equals(first, second))
166 assert.strictEqual(Hash.hash(first), Hash.hash(second))
167 assert.isFalse(Equal.equals(first, network("2001:db8::/33")))
168 })
169
170 it("computes first, last, and exact address counts", () => {
171 const ipv4All = network("0.0.0.0/0")
172 const ipv6All = network("::/0")
173 assert.strictEqual(NetAddress.formatIp(IpNetwork.firstAddress(ipv4All)), "0.0.0.0")
174 assert.strictEqual(NetAddress.formatIp(IpNetwork.lastAddress(ipv4All)), "255.255.255.255")
175 assert.strictEqual(IpNetwork.addressCount(ipv4All), BigInt(1) << BigInt(32))
176 assert.strictEqual(NetAddress.formatIp(IpNetwork.lastAddress(ipv6All)), "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
177 assert.strictEqual(IpNetwork.addressCount(ipv6All), BigInt(1) << BigInt(128))
178
179 const ipv4Pair = network("192.0.2.0/31")
180 assert.strictEqual(NetAddress.formatIp(IpNetwork.lastAddress(ipv4Pair)), "192.0.2.1")
181 assert.strictEqual(IpNetwork.addressCount(ipv4Pair), BigInt(2))
182 assert.strictEqual(NetAddress.formatIp(IpNetwork.lastAddress(network("192.0.2.128/25"))), "192.0.2.255")
183 const ipv6Pair = network("2001:db8::/127")
184 assert.strictEqual(NetAddress.formatIp(IpNetwork.lastAddress(ipv6Pair)), "2001:db8::1")
185 assert.strictEqual(IpNetwork.addressCount(ipv6Pair), BigInt(2))
186 assert.strictEqual(
187 NetAddress.formatIp(IpNetwork.lastAddress(network("2001:db8:0:1:8000::/65"))),
188 "2001:db8:0:1:ffff:ffff:ffff:ffff"
189 )
190
191 const ipv4Host = network("255.255.255.255/32")
192 assert.strictEqual(NetAddress.formatIp(IpNetwork.lastAddress(ipv4Host)), "255.255.255.255")
193 assert.strictEqual(IpNetwork.addressCount(ipv4Host), BigInt(1))
194 })
195
196 it("checks address containment in data-first and data-last forms", () => {
197 const self = network("192.0.2.0/25")
198 assert.isTrue(IpNetwork.contains(self, ip("192.0.2.0")))
199 assert.isTrue(IpNetwork.contains(self, ip("192.0.2.64")))
200 assert.isTrue(IpNetwork.contains(ip("192.0.2.127"))(self))
201 assert.isFalse(IpNetwork.contains(self, ip("192.0.1.255")))
202 assert.isFalse(IpNetwork.contains(self, ip("192.0.2.128")))
203 assert.isFalse(IpNetwork.contains(self, ip("::ffff:192.0.2.1")))
204 })
205
206 it("checks network containment and overlap", () => {
207 const parent = network("2001:db8::/63")
208 const child = network("2001:db8:0:1:8000::/65")
209 const sibling = network("2001:db8:0:1::/65")
210 const disjoint = network("2001:db8:0:2::/63")
211 assert.isTrue(IpNetwork.containsNetwork(parent, parent))
212 assert.isTrue(IpNetwork.containsNetwork(child)(parent))
213 assert.isFalse(IpNetwork.containsNetwork(child, parent))
214 assert.isTrue(IpNetwork.overlaps(parent, child))
215 assert.isTrue(IpNetwork.overlaps(child, parent))
216 assert.isFalse(IpNetwork.overlaps(child, sibling))
217 assert.isFalse(IpNetwork.overlaps(parent, disjoint))
218 assert.isFalse(IpNetwork.overlaps(parent, network("0.0.0.0/0")))
219 })
220
221 it("provides checked unsafe wrappers", () => {
222 assert.strictEqual(IpNetwork.format(IpNetwork.fromAddressUnsafe(ip("10.1.2.3"), 8)), "10.0.0.0/8")
223 assert.strictEqual(IpNetwork.format(IpNetwork.fromStringUnsafe("10.0.0.0/8")), "10.0.0.0/8")
224 assert.throws(() => IpNetwork.makeUnsafe(ip("10.1.2.3"), 8))
225 assert.throws(() => IpNetwork.fromAddressUnsafe(ip("10.1.2.3"), 33))
226 assert.throws(() => IpNetwork.fromStringUnsafe("10.1.2.3/8"))
227 })
228
229 it("decodes declarations and canonically encodes Schema strings", () => {
230 const ipv6 = Schema.decodeUnknownSync(Schema.Ipv6NetworkFromString)(
231 "2001:0DB8:0000:0000:0000:0000:0000:0000/32"
232 )
233 assert.strictEqual(Schema.encodeSync(Schema.Ipv6NetworkFromString)(ipv6), "2001:db8::/32")
234 assert.strictEqual(
235 IpNetwork.format(Schema.decodeUnknownSync(Schema.IpNetworkFromString)("192.0.2.0/24")),
236 "192.0.2.0/24"
237 )
238 assert.throws(() => Schema.decodeUnknownSync(Schema.IpNetworkFromString)("192.0.2.1/24"))
239 assert.throws(() =>
240 Schema.decodeUnknownSync(Schema.IpNetwork)({ _tag: "IpNetwork", address: ip("192.0.2.0"), prefixLength: 24 })
241 )
242 })
243
244 it("matches independent range models for generated networks", () => {
245 fc.assert(fc.property(
246 fc.tuple(
247 fc.integer({ min: 0, max: 255 }),
248 fc.integer({ min: 0, max: 255 }),
249 fc.integer({ min: 0, max: 255 }),
250 fc.integer({ min: 0, max: 255 }),
251 fc.integer({ min: 0, max: 32 })
252 ),
253 ([a, b, c, d, prefix]) => {
254 const address = success(NetAddress.ipv4FromOctets([a, b, c, d]))
255 const value = success(IpNetwork.fromAddress(address, prefix))
256 const input = (((a * 256 + b) * 256 + c) * 256 + d) >>> 0
257 const hostBits = 32 - prefix
258 const hostMask = hostBits === 32 ? 0xffffffff : 2 ** hostBits - 1
259 const first = (input & ~hostMask) >>> 0
260 const last = (first | hostMask) >>> 0
261 const parsed = success(IpNetwork.fromString(IpNetwork.format(value)))
262 const firstAddress = IpNetwork.firstAddress(value)
263 const lastAddress = IpNetwork.lastAddress(value)
264 if (!NetAddress.isIpv4Address(firstAddress) || !NetAddress.isIpv4Address(lastAddress)) {
265 assert.fail("expected IPv4 bounds")
266 }
267 assert.isTrue(Equal.equals(parsed, value))
268 assert.deepStrictEqual(NetAddress.ipv4ToOctets(firstAddress), [
269 first >>> 24,
270 (first >>> 16) & 0xff,
271 (first >>> 8) & 0xff,
272 first & 0xff
273 ])
274 assert.deepStrictEqual(NetAddress.ipv4ToOctets(lastAddress), [
275 last >>> 24,
276 (last >>> 16) & 0xff,
277 (last >>> 8) & 0xff,
278 last & 0xff
279 ])
280 assert.strictEqual(IpNetwork.addressCount(value), BigInt(2) ** BigInt(hostBits))
281 assert.isTrue(IpNetwork.contains(value, address))
282 }
283 ))
284
285 fc.assert(fc.property(
286 fc.tuple(
287 fc.array(fc.integer({ min: 0, max: 0xffff }), { minLength: 8, maxLength: 8 }),
288 fc.integer({ min: 0, max: 128 })
289 ),
290 ([segments, prefix]) => {
291 const address = success(NetAddress.ipv6FromSegments(
292 segments as [number, number, number, number, number, number, number, number]
293 ))
294 const value = success(IpNetwork.fromAddress(address, prefix))
295 const input = segments.reduce((value, segment) => (value << BigInt(16)) | BigInt(segment), BigInt(0))
296 const hostBits = BigInt(128 - prefix)
297 const hostMask = (BigInt(1) << hostBits) - BigInt(1)
298 const toBigInt = (address: NetAddress.Ipv6Address) =>
299 NetAddress.ipv6ToSegments(address).reduce(
300 (value, segment) => (value << BigInt(16)) | BigInt(segment),
301 BigInt(0)
302 )
303 const firstAddress = IpNetwork.firstAddress(value)
304 const lastAddress = IpNetwork.lastAddress(value)
305 if (!NetAddress.isIpv6Address(firstAddress) || !NetAddress.isIpv6Address(lastAddress)) {
306 assert.fail("expected IPv6 bounds")
307 }
308 assert.isTrue(Equal.equals(success(IpNetwork.fromString(IpNetwork.format(value))), value))
309 assert.strictEqual(toBigInt(firstAddress), input & ~hostMask)
310 assert.strictEqual(toBigInt(lastAddress), input | hostMask)
311 assert.strictEqual(IpNetwork.addressCount(value), BigInt(1) << hostBits)
312 assert.isTrue(IpNetwork.contains(value, address))
313 }
314 ))
315 })
316})