1import { assert, describe, it } from "@effect/vitest"
2import { assertTrue } from "@effect/vitest/utils"
3import { Equal, Hash, Option, Result, Schema } from "effect"
4import * as NetAddress from "effect/net/NetAddress"
5import { Buffer } from "node:buffer"
6import { inspect } from "node:util"
7
8const success = <A>(result: Result.Result<A, unknown>): A => {
9 assertTrue(Result.isSuccess(result), "expected Success")
10 return result.success
11}
12
13const failure = <E>(result: Result.Result<unknown, E>): E => {
14 assertTrue(Result.isFailure(result), "expected Failure")
15 return result.failure
16}
17
18describe("NetAddress", () => {
19 it("returns IP address widths", () => {
20 assert.strictEqual(NetAddress.width(success(NetAddress.ipv4FromString("1.2.3.4"))), 32)
21 assert.strictEqual(NetAddress.width(success(NetAddress.ipv6FromString("2001:db8::1"))), 128)
22 })
23
24 describe("IPv4", () => {
25 it("parses strict dotted decimal", () => {
26 assert.strictEqual(NetAddress.formatIp(success(NetAddress.ipv4FromString("1.2.3.4"))), "1.2.3.4")
27 assert.deepStrictEqual(NetAddress.ipv4ToOctets(success(NetAddress.ipv4FromOctets([127, 0, 0, 1]))), [
28 127,
29 0,
30 0,
31 1
32 ])
33 })
34
35 it("rejects malformed inputs", () => {
36 for (
37 const input of [
38 "",
39 "127.1",
40 "127.0.0.1.2",
41 ".1.2.3",
42 "1..2.3",
43 "-1.2.3.4",
44 "+1.2.3.4",
45 " 1.2.3.4",
46 "01.2.3.4",
47 "001.002.003.004",
48 "0x7f.0.0.1",
49 "256.0.0.1"
50 ]
51 ) {
52 failure(NetAddress.ipv4FromString(input))
53 }
54 failure(NetAddress.ipv4FromOctets([0, 0, 0, 256]))
55 failure(NetAddress.ipv4FromOctets([0, 0, 0, 1.5]))
56 })
57 })
58
59 describe("IPv6", () => {
60 const cases = [
61 ["0:0:0:0:0:0:0:0", "::"],
62 ["0:0:0:0:0:0:0:1", "::1"],
63 ["2001:0DB8:0000:0000:0001:0000:0000:0001", "2001:db8::1:0:0:1"],
64 ["2001:db8:0:1:0:0:0:1", "2001:db8:0:1::1"],
65 ["2001:0:0:1:0:0:1:1", "2001::1:0:0:1:1"],
66 ["::ffff:192.0.2.128", "::ffff:192.0.2.128"],
67 ["::192.0.2.128", "::c000:280"],
68 ["2001:db8::192.0.2.1", "2001:db8::c000:201"]
69 ] as const
70
71 it("uses canonical compression and embedded IPv4 policy", () => {
72 for (const [input, expected] of cases) {
73 const parsed = success(NetAddress.ipv6FromString(input))
74 assert.strictEqual(NetAddress.formatIp(parsed), expected)
75 assert.isTrue(Equal.equals(parsed, success(NetAddress.ipv6FromString(expected))))
76 }
77 })
78
79 it("rejects malformed inputs", () => {
80 for (
81 const input of [
82 "",
83 ":",
84 ":::1",
85 "1::2::3",
86 "1:2:3:4:5:6:7",
87 "1:2:3:4:5:6:7:8:9",
88 "1:2:3:4:5:6:7:8::",
89 "1:2:3:4:5:6:7::8",
90 "gggg::1",
91 "[::1]",
92 "fe80::1%1",
93 "::ffff:999.1.1.1",
94 "::ffff:192.000.2.1",
95 "::ffff:1.2.3",
96 "192.0.2.1::"
97 ]
98 ) {
99 failure(NetAddress.ipv6FromString(input))
100 }
101 failure(NetAddress.ipv6FromSegments([0, 0, 0, 0, 0, 0, 0, 65536]))
102 })
103
104 it("round trips constructed numeric values", () => {
105 const values = [
106 success(NetAddress.ipv6FromSegments([0, 0, 0, 0, 0, 0, 0, 0])),
107 success(NetAddress.ipv6FromSegments([0x2001, 0xdb8, 0, 1, 2, 3, 4, 5])),
108 success(NetAddress.ipv6FromSegments([0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff]))
109 ]
110 for (const value of values) {
111 const encoded = NetAddress.formatIp(value)
112 assert.isTrue(Equal.equals(success(NetAddress.ipv6FromString(encoded)), value))
113 }
114 })
115
116 it("uses numeric equality and hash laws", () => {
117 const expanded = success(NetAddress.ipv6FromString("2001:db8:0:0:0:0:0:1"))
118 const compressed = success(NetAddress.ipv6FromString("2001:db8::1"))
119 assert.isTrue(Equal.equals(expanded, compressed))
120 assert.strictEqual(Hash.hash(expanded), Hash.hash(compressed))
121 })
122
123 it("converts mapped IPv4 addresses", () => {
124 const ipv4 = success(NetAddress.ipv4FromString("192.0.2.128"))
125 const mapped = NetAddress.toIpv4Mapped(ipv4)
126 assert.isTrue(NetAddress.isIpv4Mapped(mapped))
127 assert.strictEqual(NetAddress.formatIp(mapped), "::ffff:192.0.2.128")
128 assert.isTrue(Equal.equals(Option.getOrThrow(NetAddress.fromIpv4Mapped(mapped)), ipv4))
129 assert.isTrue(Option.isNone(NetAddress.fromIpv4Mapped(success(NetAddress.ipv6FromString("::1")))))
130 assert.isTrue(Equal.equals(NetAddress.toCanonical(mapped), ipv4))
131 assert.isTrue(Equal.equals(NetAddress.toCanonical(ipv4), ipv4))
132 })
133
134 it("returns IPv6 octets", () => {
135 assert.deepStrictEqual(NetAddress.ipv6ToOctets(success(NetAddress.ipv6FromString("2001:db8::1"))), [
136 0x20,
137 0x01,
138 0x0d,
139 0xb8,
140 0,
141 0,
142 0,
143 0,
144 0,
145 0,
146 0,
147 0,
148 0,
149 0,
150 0,
151 1
152 ])
153 assert.strictEqual(
154 NetAddress.formatIp(NetAddress.ipv6FromBytesUnsafe(new Uint8Array(16))),
155 "::"
156 )
157 assert.strictEqual(
158 NetAddress.formatIp(NetAddress.ipv4FromBytesUnsafe(new Uint8Array([127, 0, 0, 1]))),
159 "127.0.0.1"
160 )
161 })
162 })
163
164 describe("MAC", () => {
165 it("parses and canonically formats six octets", () => {
166 const address = success(NetAddress.macAddressFromString("02:0A:0b:0C:0d:0E"))
167 assert.strictEqual(NetAddress.formatMacAddress(address), "02:0a:0b:0c:0d:0e")
168 const octets = NetAddress.macAddressToOctets(address)
169 assert.deepStrictEqual(octets, [2, 10, 11, 12, 13, 14])
170 Reflect.set(octets, 0, 0xff)
171 assert.strictEqual(NetAddress.formatMacAddress(address), "02:0a:0b:0c:0d:0e")
172 assert.isTrue(NetAddress.isMacAddress(address))
173 assert.isTrue(Equal.equals(address, success(NetAddress.macAddressFromOctets([2, 10, 11, 12, 13, 14]))))
174 assert.strictEqual(Hash.hash(address), Hash.hash(success(NetAddress.macAddressFromString("02:0a:0b:0c:0d:0e"))))
175 })
176
177 it("rejects malformed inputs and invalid octets", () => {
178 for (
179 const input of [
180 "",
181 "00:11:22:33:44",
182 "00:11:22:33:44:55:66",
183 "0:11:22:33:44:55",
184 "00-11-22-33-44-55",
185 "0011.2233.4455",
186 "gg:11:22:33:44:55"
187 ]
188 ) {
189 failure(NetAddress.macAddressFromString(input))
190 }
191 failure(NetAddress.macAddressFromOctets([0, 0, 0, 0, 0, 256]))
192 failure(NetAddress.macAddressFromOctets([0, 0, 0, 0, 0, 1.5]))
193 assert.throws(() => NetAddress.macAddressFromStringUnsafe("invalid"))
194 })
195
196 it("classifies address bits", () => {
197 const mac = (text: string) => success(NetAddress.macAddressFromString(text))
198 assert.isTrue(NetAddress.isMacBroadcast(mac("ff:ff:ff:ff:ff:ff")))
199 assert.isTrue(NetAddress.isMacMulticast(mac("01:00:5e:00:00:01")))
200 assert.isFalse(NetAddress.isMacUnicast(mac("01:00:5e:00:00:01")))
201 assert.isTrue(NetAddress.isMacUnicast(mac("00:00:5e:00:53:01")))
202 assert.isTrue(NetAddress.isMacLocallyAdministered(mac("02:00:00:00:00:01")))
203 assert.isFalse(NetAddress.isMacUniversallyAdministered(mac("02:00:00:00:00:01")))
204 assert.isTrue(NetAddress.isMacUniversallyAdministered(mac("00:00:5e:00:53:01")))
205 })
206 })
207
208 it("classifies documented address ranges at their boundaries", () => {
209 const ip = (text: string) => success(NetAddress.ipFromString(text))
210 assert.isTrue(NetAddress.isUnspecified(ip("0.0.0.0")))
211 assert.isTrue(NetAddress.isUnspecified(ip("::")))
212 assert.isTrue(NetAddress.isLoopback(ip("127.255.255.255")))
213 assert.isFalse(NetAddress.isLoopback(ip("128.0.0.0")))
214 assert.isTrue(NetAddress.isMulticast(ip("224.0.0.0")))
215 assert.isFalse(NetAddress.isMulticast(ip("223.255.255.255")))
216 assert.isTrue(NetAddress.isMulticast(ip("ff00::")))
217 assert.isTrue(NetAddress.isBroadcast(NetAddress.ipv4Broadcast))
218 assert.isFalse(NetAddress.isBroadcast(success(NetAddress.ipv4FromString("255.255.255.254"))))
219 assert.isTrue(NetAddress.isLinkLocal(ip("169.254.255.255")))
220 assert.isFalse(NetAddress.isLinkLocal(ip("169.255.0.0")))
221 assert.isTrue(NetAddress.isLinkLocal(ip("febf::")))
222 assert.isFalse(NetAddress.isLinkLocal(ip("fec0::")))
223 assert.isTrue(NetAddress.isPrivate(success(NetAddress.ipv4FromString("10.255.255.255"))))
224 assert.isTrue(NetAddress.isPrivate(success(NetAddress.ipv4FromString("172.31.255.255"))))
225 assert.isTrue(NetAddress.isPrivate(success(NetAddress.ipv4FromString("192.168.0.0"))))
226 assert.isFalse(NetAddress.isPrivate(success(NetAddress.ipv4FromString("172.32.0.0"))))
227 assert.isTrue(NetAddress.isUniqueLocal(success(NetAddress.ipv6FromString("fc00::"))))
228 assert.isTrue(
229 NetAddress.isUniqueLocal(success(NetAddress.ipv6FromString("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")))
230 )
231 assert.isFalse(NetAddress.isUniqueLocal(success(NetAddress.ipv6FromString("fe00::"))))
232 assert.isTrue(Equal.equals(NetAddress.ipv4Unspecified, ip("0.0.0.0")))
233 assert.isTrue(Equal.equals(NetAddress.ipv6Unspecified, ip("::")))
234 })
235
236 describe("branded classifications", () => {
237 const ip = NetAddress.ipFromStringUnsafe
238 const ipv4 = (text: string) => success(NetAddress.ipv4FromString(text))
239 const ipv6 = (text: string) => success(NetAddress.ipv6FromString(text))
240 const mac = NetAddress.macAddressFromStringUnsafe
241
242 it("classifies IP boundaries without claiming reachability", () => {
243 for (const text of ["1.2.3.4", "127.0.0.1", "169.254.1.1", "240.0.0.1", "::1", "fe80::1"]) {
244 assert.isTrue(NetAddress.isUnicast(ip(text)), text)
245 }
246 for (const text of ["0.0.0.0", "224.0.0.0", "255.255.255.255", "::", "ff00::"]) {
247 assert.isFalse(NetAddress.isUnicast(ip(text)), text)
248 }
249 assert.isTrue(NetAddress.isLoopback(ip("127.0.0.0")))
250 assert.isTrue(NetAddress.isLoopback(ip("127.255.255.255")))
251 assert.isFalse(NetAddress.isLoopback(ip("128.0.0.0")))
252 assert.isTrue(NetAddress.isLoopback(ip("::1")))
253 assert.isFalse(NetAddress.isLoopback(ip("::2")))
254 assert.isFalse(NetAddress.isLoopback(ip("::ffff:127.0.0.1")))
255 assert.isFalse(NetAddress.isMulticast(ip("::ffff:224.0.0.1")))
256 assert.isTrue(NetAddress.isLinkLocal(ip("169.254.0.0")))
257 assert.isTrue(NetAddress.isLinkLocal(ip("169.254.255.255")))
258 assert.isFalse(NetAddress.isLinkLocal(ip("169.255.0.0")))
259 assert.isTrue(NetAddress.isLinkLocal(ip("fe80::")))
260 assert.isTrue(NetAddress.isLinkLocal(ip("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")))
261 assert.isFalse(NetAddress.isLinkLocal(ip("fec0::")))
262 for (const text of ["10.0.0.0", "10.255.255.255", "172.16.0.0", "172.31.255.255", "192.168.0.0"]) {
263 assert.isTrue(NetAddress.isPrivate(ipv4(text)), text)
264 }
265 for (const text of ["9.255.255.255", "172.15.255.255", "172.32.0.0", "192.169.0.0", "100.64.0.1"]) {
266 assert.isFalse(NetAddress.isPrivate(ipv4(text)), text)
267 }
268 assert.isTrue(NetAddress.isUniqueLocal(ipv6("fc00::")))
269 assert.isTrue(NetAddress.isUniqueLocal(ipv6("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")))
270 assert.isFalse(NetAddress.isUniqueLocal(ipv6("fe00::")))
271 })
272
273 it("classifies the independent MAC group and administration bits", () => {
274 const cases = [
275 ["00:00:5e:00:53:01", false, false],
276 ["01:00:5e:00:00:01", true, false],
277 ["02:00:00:00:00:01", false, true],
278 ["03:00:00:00:00:01", true, true],
279 ["ff:ff:ff:ff:ff:ff", true, true]
280 ] as const
281 for (const [text, multicast, local] of cases) {
282 const address = mac(text)
283 assert.strictEqual(NetAddress.isMulticast(address), multicast)
284 assert.strictEqual(NetAddress.isMacMulticast(address), multicast)
285 assert.strictEqual(NetAddress.isUnicast(address), !multicast)
286 assert.strictEqual(NetAddress.isMacUnicast(address), !multicast)
287 assert.strictEqual(NetAddress.isMacLocallyAdministered(address), local)
288 assert.strictEqual(NetAddress.isMacUniversallyAdministered(address), !local)
289 }
290 const broadcast = mac("ff:ff:ff:ff:ff:ff")
291 assert.isTrue(NetAddress.isBroadcast(broadcast))
292 assert.isTrue(NetAddress.isMacBroadcast(broadcast))
293 assert.isTrue(NetAddress.isMulticast(broadcast))
294 assert.isFalse(NetAddress.isBroadcast(mac("01:00:5e:00:00:01")))
295 assert.isTrue(NetAddress.isBroadcast(NetAddress.ipv4Broadcast))
296 assert.isFalse(NetAddress.isMulticast(NetAddress.ipv4Broadcast))
297 })
298 })
299
300 it("serializes and inspects canonical strings without private bytes", () => {
301 const cases = [
302 [NetAddress.ipv4Loopback, "127.0.0.1"],
303 [NetAddress.ipv6Loopback, "::1"],
304 [NetAddress.macAddressFromStringUnsafe("02:0A:0B:0C:0D:0E"), "02:0a:0b:0c:0d:0e"],
305 [NetAddress.inetAddressFromStringUnsafe("127.0.0.1:80"), "127.0.0.1:80"],
306 [NetAddress.inetAddressFromStringUnsafe("[fe80::1%2]:80"), "[fe80::1%2]:80"],
307 [NetAddress.unixPathAddress("./run/../server.sock"), "./run/../server.sock"]
308 ] as const
309 for (const [address, expected] of cases) {
310 assert.strictEqual(address.toJSON(), expected)
311 assert.strictEqual(JSON.stringify(address), JSON.stringify(expected))
312 assert.strictEqual(JSON.stringify({ address }), JSON.stringify({ address: expected }))
313 assert.strictEqual(inspect(address), expected)
314 }
315 })
316
317 it("preserves the input rejected by the failing operation", () => {
318 const cases = [
319 [NetAddress.ipv4FromString, "01.2.3.4", "01.2.3.4"],
320 [NetAddress.ipv6FromString, "::ffff:192.000.2.1", "192.000.2.1"],
321 [NetAddress.macAddressFromString, "00-11-22-33-44-55", "00-11-22-33-44-55"],
322 [NetAddress.inetAddressFromString, "localhost:80", "localhost"],
323 [NetAddress.inetAddressFromString, "[::ffff:999.0.0.1]:80", "999.0.0.1"],
324 [NetAddress.inetAddressFromString, "[fe80::1%bad]:80", "[fe80::1%bad]:80"]
325 ] as const
326 for (const [parse, input, rejected] of cases) {
327 assert.strictEqual(failure(parse(input)).input, rejected)
328 }
329 assert.deepStrictEqual(failure(NetAddress.inetAddressFromString("127.0.0.1:65536")).input, NetAddress.ipv4Loopback)
330 assert.strictEqual(
331 failure(NetAddress.socketAddressFromInput({ address: "localhost", port: 80 })).input,
332 "localhost"
333 )
334 assert.strictEqual(failure(NetAddress.inetAddressFromIpString("localhost", 80)).input, "localhost")
335 const octets = [256, 0, 0, 1] as const
336 assert.strictEqual(failure(NetAddress.ipv4FromOctets(octets)).input, octets)
337 })
338
339 it("retains the address supplied to checked socket constructors", () => {
340 const ipv4 = NetAddress.ipv4Loopback
341 const ipv6 = NetAddress.ipv6Loopback
342 assert.strictEqual(failure(NetAddress.inetAddressV4(ipv4, -1)).input, ipv4)
343 assert.strictEqual(failure(NetAddress.inetAddressV6(ipv6, -1)).input, ipv6)
344 assert.strictEqual(failure(NetAddress.inetAddressV6(ipv6, 80, { scopeId: -1 })).input, ipv6)
345 })
346
347 it("converts internet addresses to normalized URL values", () => {
348 for (
349 const [input, scheme, expected] of [
350 ["[::]:3000", "https", "https://[::]:3000/"],
351 ["0.0.0.0:3000", "http", "http://0.0.0.0:3000/"],
352 ["[::1]:3000", "ws", "ws://[::1]:3000/"],
353 ["192.0.2.1:80", "http", "http://192.0.2.1/"],
354 ["[::1]:443", "HTTPS", "https://[::1]/"],
355 ["[::1]:0", "http", "http://[::1]:0/"],
356 ["192.0.2.1:65535", "http", "http://192.0.2.1:65535/"]
357 ]
358 ) {
359 const address = NetAddress.inetAddressFromStringUnsafe(input)
360 const url = success(NetAddress.toUrl(address, scheme))
361 assert.instanceOf(url, URL)
362 assert.strictEqual(url.href, expected)
363 }
364 const address = NetAddress.inetAddressFromStringUnsafe("[::]:3000")
365 const url = success(NetAddress.toUrl(address))
366 assert.strictEqual(url.href, "http://[::]:3000/")
367 url.hostname = "127.0.0.1"
368 assert.strictEqual(url.origin, "http://127.0.0.1:3000")
369 assert.strictEqual(success(NetAddress.toUrl(address, undefined)).hostname, "[::]")
370 })
371
372 it("converts bare IP addresses without adding an explicit port", () => {
373 for (
374 const [input, expected] of [
375 ["127.0.0.1", "http://127.0.0.1/"],
376 ["::1", "http://[::1]/"],
377 ["0.0.0.0", "http://0.0.0.0/"],
378 ["::", "http://[::]/"]
379 ]
380 ) {
381 const url = success(NetAddress.toUrl(NetAddress.ipFromStringUnsafe(input)))
382 assert.strictEqual(url.href, expected)
383 assert.strictEqual(url.port, "")
384 }
385 assert.strictEqual(success(NetAddress.toUrl(NetAddress.ipv6Loopback, "https")).href, "https://[::1]/")
386 })
387
388 it("formats URL strings without trailing slashes or default ports", () => {
389 assert.strictEqual(success(NetAddress.formatUrl(NetAddress.ipv4Loopback)), "http://127.0.0.1")
390 assert.strictEqual(success(NetAddress.formatUrl(NetAddress.ipv6Loopback, "https")), "https://[::1]")
391 assert.strictEqual(NetAddress.formatUrlUnsafe(NetAddress.ipv4Loopback), "http://127.0.0.1")
392 assert.strictEqual(NetAddress.formatUrlUnsafe(NetAddress.ipv6Loopback, "https"), "https://[::1]")
393 for (
394 const [input, scheme, expected] of [
395 ["192.0.2.1:80", "http", "http://192.0.2.1"],
396 ["[::1]:443", "HTTPS", "https://[::1]"],
397 ["[::]:3000", "http", "http://[::]:3000"],
398 ["[::1]:0", "http", "http://[::1]:0"],
399 ["127.0.0.1:80", "tcp", "tcp://127.0.0.1:80"]
400 ]
401 ) {
402 assert.strictEqual(success(NetAddress.formatUrl(NetAddress.inetAddressFromStringUnsafe(input), scheme)), expected)
403 }
404 const scoped = NetAddress.inetAddressFromStringUnsafe("[fe80::1%2]:3000")
405 const scopedError = failure(NetAddress.formatUrl(scoped))
406 assert.strictEqual(scopedError.input, scoped)
407 assert.include(scopedError.message, "scoped IPv6")
408 const schemeError = failure(NetAddress.formatUrl(NetAddress.ipv4Loopback, "1http"))
409 assert.strictEqual(schemeError.input, NetAddress.ipv4Loopback)
410 assert.instanceOf(schemeError.cause, TypeError)
411 assert.throws(() => NetAddress.formatUrlUnsafe(scoped), NetAddress.NetAddressError, "scoped IPv6")
412 assert.throws(() => NetAddress.formatUrlUnsafe(NetAddress.ipv4Loopback, "1http"), NetAddress.NetAddressError)
413 })
414
415 it("formats Unix socket paths without encoding or normalizing them", () => {
416 for (
417 const [path, expected] of [
418 ["/tmp/server.sock", "unix:///tmp/server.sock"],
419 ["./run/../server.sock", "unix://./run/../server.sock"],
420 ["/tmp/socket?#%20", "unix:///tmp/socket?#%20"],
421 ["/tmp/socket with spaces.sock", "unix:///tmp/socket with spaces.sock"],
422 ["", "unix://"]
423 ]
424 ) {
425 const address = NetAddress.unixPathAddress(path)
426 assert.strictEqual(NetAddress.formatUnixPath(address), expected)
427 assert.strictEqual(success(NetAddress.formatUrl(address)), expected)
428 assert.strictEqual(NetAddress.formatUrlUnsafe(address, "https"), expected)
429 }
430 })
431
432 it("returns URL conversion errors for scopes and rejected URLs", () => {
433 for (const input of ["[fe80::1%2]:3000", "[::%2]:3000"]) {
434 const scoped = NetAddress.inetAddressFromStringUnsafe(input)
435 const error = failure(NetAddress.toUrl(scoped, "http"))
436 assert.instanceOf(error, NetAddress.NetAddressError)
437 assert.strictEqual(error.input, scoped)
438 assert.include(error.message, "scoped IPv6")
439 assert.isUndefined(error.cause)
440 }
441 const address = NetAddress.inetAddressFromStringUnsafe("127.0.0.1:80")
442 for (const scheme of ["", "1http", "file"]) {
443 const error = failure(NetAddress.toUrl(address, scheme))
444 assert.instanceOf(error, NetAddress.NetAddressError)
445 assert.strictEqual(error.input, address)
446 assert.strictEqual(error.message, "failed to construct URL")
447 assert.instanceOf(error.cause, TypeError)
448 }
449 })
450
451 it("canonicalizes mapped internet addresses while retaining their port", () => {
452 const ipv4 = success(NetAddress.ipv4FromString("192.0.2.128"))
453 const expected = success(NetAddress.inetAddressV4(ipv4, 4567))
454 for (const scopeId of [0, 7]) {
455 const mapped = success(NetAddress.inetAddressV6(NetAddress.toIpv4Mapped(ipv4), 4567, { scopeId }))
456 const canonical: NetAddress.InetAddress = NetAddress.toCanonical(mapped)
457 assert.isTrue(NetAddress.isInetAddressV4(canonical))
458 assert.isTrue(Equal.equals(canonical, expected))
459 assert.strictEqual(Hash.hash(canonical), Hash.hash(expected))
460 assert.strictEqual(NetAddress.toCanonical(canonical), canonical)
461 }
462 })
463
464 it("retains identity and scope for internet addresses that need no canonicalization", () => {
465 const scoped = success(
466 NetAddress.inetAddressV6(success(NetAddress.ipv6FromString("fe80::1")), 1234, { scopeId: 3 })
467 )
468 for (const address of [success(NetAddress.inetAddressV4(NetAddress.ipv4Loopback, 1234)), scoped]) {
469 assert.strictEqual(NetAddress.toCanonical(address), address)
470 }
471 assert.strictEqual(NetAddress.toCanonical(scoped.address), scoped.address)
472 })
473
474 it("constructs immutable address values", () => {
475 const ipv4 = success(NetAddress.ipv4FromString("127.0.0.1"))
476 const ipv6 = success(NetAddress.ipv6FromString("::1"))
477 const inet4 = success(NetAddress.inetAddressV4(ipv4, 80))
478 const inet6 = success(NetAddress.inetAddressV6(ipv6, 80, { scopeId: 2 }))
479 const mac = success(NetAddress.macAddressFromString("00:00:5e:00:53:01"))
480 const unix = NetAddress.unixPathAddress("server.sock")
481
482 for (const address of [ipv4, ipv6, inet4, inet6, mac, unix]) {
483 assert.isTrue(Object.isFrozen(address))
484 }
485 })
486
487 it("copies byte constructor inputs", () => {
488 const ipv4Bytes = new Uint8Array([127, 0, 0, 1])
489 const ipv6Bytes = new Uint8Array(16)
490 ipv6Bytes[15] = 1
491 const ipv4 = NetAddress.ipv4FromBytesUnsafe(ipv4Bytes)
492 const ipv6 = NetAddress.ipv6FromBytesUnsafe(ipv6Bytes)
493 const ipv4Hash = Hash.hash(ipv4)
494 const ipv6Hash = Hash.hash(ipv6)
495
496 ipv4Bytes[0] = 1
497 ipv6Bytes[15] = 2
498
499 assert.strictEqual(NetAddress.formatIp(ipv4), "127.0.0.1")
500 assert.strictEqual(NetAddress.formatIp(ipv6), "::1")
501 assert.strictEqual(Hash.hash(ipv4), ipv4Hash)
502 assert.strictEqual(Hash.hash(ipv6), ipv6Hash)
503 })
504
505 it("copies Buffer constructor inputs", () => {
506 const ipv4Bytes = Buffer.from([127, 0, 0, 1])
507 const ipv6Bytes = Buffer.alloc(16)
508 ipv6Bytes[15] = 1
509 const ipv4 = NetAddress.ipv4FromBytesUnsafe(ipv4Bytes)
510 const ipv6 = NetAddress.ipv6FromBytesUnsafe(ipv6Bytes)
511 const ipv4Hash = Hash.hash(ipv4)
512 const ipv6Hash = Hash.hash(ipv6)
513
514 ipv4Bytes[0] = 1
515 ipv6Bytes[15] = 2
516
517 assert.strictEqual(NetAddress.formatIp(ipv4), "127.0.0.1")
518 assert.strictEqual(NetAddress.formatIp(ipv6), "::1")
519 assert.strictEqual(Hash.hash(ipv4), ipv4Hash)
520 assert.strictEqual(Hash.hash(ipv6), ipv6Hash)
521 assert.isTrue(Equal.equals(ipv4, NetAddress.ipv4Loopback))
522 assert.isTrue(Equal.equals(ipv6, NetAddress.ipv6Loopback))
523 })
524
525 it("discriminates addresses sharing the common type id", () => {
526 assert.isTrue(NetAddress.isIpv4Address(NetAddress.ipv4Loopback))
527 assert.isFalse(NetAddress.isIpv6Address(NetAddress.ipv4Loopback))
528 assert.isFalse(NetAddress.isMacAddress(NetAddress.ipv4Loopback))
529 })
530
531 it("returns NetAddressError from checked operations", () => {
532 const result = NetAddress.ipFromString("localhost")
533 assertTrue(Result.isFailure(result), "expected Failure")
534 assert.instanceOf(result.failure, NetAddress.NetAddressError)
535 assert.strictEqual(result.failure.message, "expected exactly four decimal octets")
536 assert.strictEqual(result.failure.input, "localhost")
537 })
538
539 describe("socket addresses", () => {
540 it("constructs socket addresses from input", () => {
541 const inet = NetAddress.inetAddressFromStringUnsafe("127.0.0.1:8080")
542 assert.strictEqual(success(NetAddress.socketAddressFromInput(inet)), inet)
543 assert.strictEqual(
544 NetAddress.formatSocketAddress(success(NetAddress.socketAddressFromInput("127.0.0.1:3000"))),
545 "127.0.0.1:3000"
546 )
547 assert.strictEqual(
548 NetAddress.formatSocketAddress(success(NetAddress.socketAddressFromInput("[::1]:3000"))),
549 "[::1]:3000"
550 )
551 assert.strictEqual(
552 NetAddress.formatSocketAddress(success(NetAddress.socketAddressFromInput({
553 address: "::1",
554 port: 8080
555 }))),
556 "[::1]:8080"
557 )
558 assert.strictEqual(
559 NetAddress.formatSocketAddress(success(NetAddress.socketAddressFromInput({
560 address: NetAddress.ipv4Loopback,
561 port: 8080
562 }))),
563 "127.0.0.1:8080"
564 )
565 assert.strictEqual(
566 NetAddress.formatSocketAddress(success(NetAddress.socketAddressFromInput({ path: "server.sock" }))),
567 "server.sock"
568 )
569 failure(NetAddress.socketAddressFromInput({ address: "localhost", port: 8080 }))
570 failure(NetAddress.socketAddressFromInput({ address: "127.0.0.1", port: 65536 }))
571 failure(NetAddress.socketAddressFromInput("localhost:8080"))
572 assert.throws(() => NetAddress.socketAddressFromInputUnsafe({ address: "localhost", port: 8080 }))
573 })
574
575 it("parses and formats bracketed numeric addresses", () => {
576 assert.strictEqual(
577 NetAddress.formatInet(success(NetAddress.inetAddressFromString("127.0.0.1:8080"))),
578 "127.0.0.1:8080"
579 )
580 assert.strictEqual(
581 NetAddress.formatInet(success(NetAddress.inetAddressFromString("[0:0:0:0:0:0:0:1]:8080"))),
582 "[::1]:8080"
583 )
584 assert.strictEqual(NetAddress.formatInet(success(NetAddress.inetAddressFromIpString("::1", 8080))), "[::1]:8080")
585 assert.strictEqual(NetAddress.formatUrlHost(success(NetAddress.ipv6FromString("::1"))), "[::1]")
586 })
587
588 it("formats separate socket hosts while preserving IPv6 scope", () => {
589 for (
590 const [input, expected] of [
591 ["127.0.0.1:8080", "127.0.0.1"],
592 ["[0:0:0:0:0:0:0:1]:8080", "::1"],
593 ["[fe80::1%7]:4567", "fe80::1%7"],
594 ["[fe80::1%0]:4567", "fe80::1"]
595 ]
596 ) {
597 const address = NetAddress.inetAddressFromStringUnsafe(input)
598 const host = NetAddress.formatHost(address)
599 assert.strictEqual(host, expected)
600 assert.deepStrictEqual(success(NetAddress.inetAddressFromHostString(host, address.port)), address)
601 }
602 })
603
604 it("formats native hosts using the supplied platform and interface names", () => {
605 const scopeIds = NetAddress.scopeIdsFromInterfaces([
606 ["en0", [{ family: "IPv6", scopeid: 7 }]],
607 ["en1", [{ family: "IPv6", scopeid: 7 }]]
608 ])
609 for (const platform of ["win32", "linux", "darwin", undefined]) {
610 for (
611 const [input, expected] of [
612 ["127.0.0.1:8080", "127.0.0.1"],
613 ["[0:0:0:0:0:0:0:1]:8080", "::1"],
614 ["[fe80::1%0]:4567", "fe80::1"],
615 ["[fe80::1%7]:4567", platform === "win32" ? "fe80::1%7" : "fe80::1%en0"],
616 ["[fe80::1%9]:4567", "fe80::1%9"]
617 ]
618 ) {
619 const address = NetAddress.inetAddressFromStringUnsafe(input)
620 const host = NetAddress.formatNativeHost(address, scopeIds, platform)
621 assert.strictEqual(host, expected)
622 assert.deepStrictEqual(success(NetAddress.inetAddressFromHostString(host, address.port, scopeIds)), address)
623 }
624 }
625 const address = NetAddress.inetAddressFromStringUnsafe("[fe80::1%7]:4567")
626 assert.strictEqual(NetAddress.formatNativeHost(address, scopeIds), "fe80::1%en0")
627 assert.strictEqual(NetAddress.formatNativeHost(address, new Map(), "linux"), "fe80::1%7")
628 assert.strictEqual(NetAddress.formatNativeHost(address, new Map([["eth0", 7]]), "linux"), "fe80::1%eth0")
629 })
630
631 it("formats multicast interfaces using the supplied platform and scope map", () => {
632 const scopeIds = NetAddress.scopeIdsFromInterfaces([
633 ["en0", [{ family: "IPv6", scopeid: 7 }]],
634 ["en1", [{ family: "IPv6", scopeid: 7 }]]
635 ])
636 for (const platform of ["win32", "linux", "darwin", undefined]) {
637 assert.strictEqual(
638 NetAddress.formatMulticastInterface(NetAddress.ipv4Loopback, scopeIds, platform),
639 "127.0.0.1"
640 )
641 assert.strictEqual(NetAddress.formatMulticastInterface(0, scopeIds, platform), "::")
642 assert.strictEqual(
643 NetAddress.formatMulticastInterface(7, scopeIds, platform),
644 platform === "win32" ? "::%7" : "::%en0"
645 )
646 assert.strictEqual(NetAddress.formatMulticastInterface(9, scopeIds, platform), "::%9")
647 }
648 assert.strictEqual(NetAddress.formatMulticastInterface(7, scopeIds), "::%en0")
649 assert.strictEqual(NetAddress.formatMulticastInterface(7, new Map(), "linux"), "::%7")
650 assert.strictEqual(NetAddress.formatMulticastInterface(7, new Map([["eth0", 7]]), "linux"), "::%eth0")
651 })
652
653 it("resolves named IPv6 zones only through the supplied scope map", () => {
654 for (const scopeId of [7, 9]) {
655 const scopeIds = new Map([["en0", scopeId]])
656 const address = success(NetAddress.inetAddressFromHostString("fe80::1%en0", 4567, scopeIds))
657 assert.deepStrictEqual(address, NetAddress.inetAddressFromStringUnsafe(`[fe80::1%${scopeId}]:4567`))
658 }
659 const unknown = failure(NetAddress.inetAddressFromHostString("fe80::1%en0", 4567, new Map([["en1", 7]])))
660 assert.strictEqual(unknown.input, "fe80::1%en0")
661 failure(NetAddress.inetAddressFromHostString("fe80::1%en0", 4567))
662 })
663
664 it("snapshots the first positive IPv6 scope ID from each interface", () => {
665 const linkLocal = { family: "IPv6", scopeid: 7 }
666 const interfaces = {
667 en0: [
668 { family: "IPv4", scopeid: 99 },
669 { family: "IPv6" },
670 { family: "IPv6", scopeid: 0 },
671 { family: "IPv6", scopeid: -1 },
672 linkLocal,
673 { family: "IPv6", scopeid: 9 }
674 ],
675 en1: undefined,
676 en2: [{ family: "IPv4" }, { family: "IPv6", scopeid: 0 }],
677 en3: [{ family: "IPv6", scopeid: 11 }]
678 }
679 const scopeIds = NetAddress.scopeIdsFromInterfaces(Object.entries(interfaces))
680 linkLocal.scopeid = 12
681 assert.deepStrictEqual(scopeIds, new Map([["en0", 7], ["en3", 11]]))
682 assert.deepStrictEqual(NetAddress.scopeIdsFromInterfaces([]), new Map())
683 })
684
685 it("provides throwing counterparts for trusted construction", () => {
686 const ip = NetAddress.ipFromStringUnsafe("::1")
687 assert.strictEqual(NetAddress.formatIp(ip), "::1")
688 assert.strictEqual(NetAddress.formatInet(NetAddress.inetAddressUnsafe(ip, 8080)), "[::1]:8080")
689 assert.strictEqual(
690 NetAddress.formatInet(NetAddress.inetAddressFromStringUnsafe("127.0.0.1:8080")),
691 "127.0.0.1:8080"
692 )
693 assert.throws(() => NetAddress.ipFromStringUnsafe("localhost"))
694 assert.throws(() => NetAddress.inetAddressUnsafe(ip, -1))
695 assert.throws(() => NetAddress.inetAddressFromStringUnsafe("localhost:8080"))
696 })
697
698 it("checks port boundaries and rejects ambiguous input", () => {
699 for (const port of [0, 1, 65535]) {
700 assert.strictEqual(success(NetAddress.inetAddressFromString(`127.0.0.1:${port}`)).port, port)
701 }
702 for (
703 const input of [
704 "127.0.0.1:-1",
705 "127.0.0.1:65536",
706 "127.0.0.1:1.5",
707 "127.0.0.1:00",
708 "127.0.0.1:080",
709 "localhost:80",
710 "::1:80",
711 "[::1]80",
712 "[127.0.0.1]:80",
713 "[fe80::1%]:80",
714 "[fe80::1%-1]:80",
715 "[fe80::1%1.5]:80",
716 "[fe80::1%en0]:80",
717 "[fe80::1%1%2]:80",
718 "[fe80::1%4294967296]:80"
719 ]
720 ) {
721 failure(NetAddress.inetAddressFromString(input))
722 }
723 const invalidPort = NetAddress.inetAddressFromString("127.0.0.1:65536")
724 assertTrue(Result.isFailure(invalidPort), "expected Failure")
725 assert.strictEqual(invalidPort.failure.message, "port must be an integer from 0 through 65535")
726
727 const missingPort = NetAddress.inetAddressFromString("127.0.0.1")
728 assertTrue(Result.isFailure(missingPort), "expected Failure")
729 assert.strictEqual(missingPort.failure.message, "expected host:port or [IPv6]:port")
730 })
731
732 it("preserves IPv6 scope metadata in equality and hashing", () => {
733 const address = success(NetAddress.ipv6FromString("fe80::1"))
734 const first = success(NetAddress.inetAddressV6(address, 80, { scopeId: 3 }))
735 const second = success(NetAddress.inetAddressV6(address, 80, { scopeId: 3 }))
736 const otherScope = success(NetAddress.inetAddressV6(address, 80, { scopeId: 4 }))
737 assert.isTrue(Equal.equals(first, second))
738 assert.strictEqual(Hash.hash(first), Hash.hash(second))
739 assert.isFalse(Equal.equals(first, otherScope))
740 })
741
742 it("round trips numeric IPv6 scope identifiers", () => {
743 const scoped = success(NetAddress.inetAddressFromString("[fe80::1%4294967295]:65535"))
744 if (!NetAddress.isInetAddressV6(scoped)) assert.fail("expected InetAddressV6")
745 assert.strictEqual(scoped.scopeId, 0xffffffff)
746 assert.strictEqual(NetAddress.formatInet(scoped), "[fe80::1%4294967295]:65535")
747 assert.strictEqual(
748 NetAddress.formatInet(success(NetAddress.inetAddressFromString("[fe80::1%0]:80"))),
749 "[fe80::1]:80"
750 )
751 })
752
753 it("keeps Unix paths opaque", () => {
754 const address = NetAddress.unixPathAddress("./run/../server.sock")
755 assert.strictEqual(address.path, "./run/../server.sock")
756 assert.strictEqual(NetAddress.formatSocketAddress(address), "./run/../server.sock")
757 assert.isTrue(Equal.equals(address, NetAddress.unixPathAddress("./run/../server.sock")))
758 })
759 })
760
761 it("decodes and canonically encodes schemas", () => {
762 assert.strictEqual(Schema.resolveAnnotations(Schema.IpAddress)?.identifier, "IpAddress")
763 assert.strictEqual(Schema.resolveAnnotations(Schema.IpAddressFromString)?.identifier, "IpAddressFromString")
764 const mac = Schema.decodeUnknownSync(Schema.MacAddressFromString)("02:0A:0b:0C:0d:0E")
765 assert.strictEqual(Schema.encodeSync(Schema.MacAddressFromString)(mac), "02:0a:0b:0c:0d:0e")
766 const ipv6 = Schema.decodeUnknownSync(Schema.Ipv6AddressFromString)("2001:0DB8:0:0:0:0:0:1")
767 assert.strictEqual(Schema.encodeSync(Schema.Ipv6AddressFromString)(ipv6), "2001:db8::1")
768 const inet = Schema.decodeUnknownSync(Schema.InetAddressFromString)("[0:0:0:0:0:0:0:1]:0")
769 assert.strictEqual(Schema.encodeSync(Schema.InetAddressFromString)(inet), "[::1]:0")
770 const scoped = Schema.decodeUnknownSync(Schema.InetAddressFromString)("[fe80::1%3]:80")
771 assert.strictEqual(Schema.encodeSync(Schema.InetAddressFromString)(scoped), "[fe80::1%3]:80")
772 const unix = Schema.decodeUnknownSync(Schema.UnixPathAddressFromString)("../opaque.sock")
773 assert.strictEqual(Schema.encodeSync(Schema.UnixPathAddressFromString)(unix), "../opaque.sock")
774 assert.throws(() => Schema.decodeUnknownSync(Schema.MacAddressFromString)("00-11-22-33-44-55"))
775 assert.throws(() => Schema.decodeUnknownSync(Schema.Ipv4AddressFromString)("999.0.0.1"))
776 })
777
778 it("validates branded address schemas on decode and encode", () => {
779 const multicastIp = Schema.decodeUnknownSync(Schema.IpMulticastAddressFromString)("239.255.0.1")
780 const multicastMac = Schema.decodeUnknownSync(Schema.MacMulticastAddressFromString)("01:00:5E:00:00:01")
781 const broadcastMac = Schema.decodeUnknownSync(Schema.MacBroadcastAddressFromString)("FF:FF:FF:FF:FF:FF")
782 assert.strictEqual(Schema.encodeSync(Schema.IpMulticastAddressFromString)(multicastIp), "239.255.0.1")
783 assert.strictEqual(
784 Schema.encodeSync(Schema.MacMulticastAddressFromString)(multicastMac),
785 "01:00:5e:00:00:01"
786 )
787 assert.isTrue(NetAddress.isMulticast(multicastIp))
788 assert.isTrue(NetAddress.isMulticast(multicastMac))
789 assert.isTrue(NetAddress.isMulticast(broadcastMac))
790
791 assert.isTrue(NetAddress.isUnicast(Schema.decodeUnknownSync(Schema.IpUnicastAddressFromString)("192.0.2.1")))
792 assert.isTrue(
793 NetAddress.isMacUnicast(Schema.decodeUnknownSync(Schema.MacUnicastAddressFromString)("00:00:5e:00:53:01"))
794 )
795 assert.isTrue(
796 NetAddress.isBroadcast(Schema.decodeUnknownSync(Schema.Ipv4BroadcastAddressFromString)("255.255.255.255"))
797 )
798 assert.isTrue(NetAddress.isLoopback(Schema.decodeUnknownSync(Schema.IpLoopbackAddressFromString)("::1")))
799 assert.isTrue(NetAddress.isLinkLocal(Schema.decodeUnknownSync(Schema.IpLinkLocalAddressFromString)("fe80::1")))
800 assert.isTrue(NetAddress.isUnspecified(Schema.decodeUnknownSync(Schema.IpUnspecifiedAddressFromString)("::")))
801 assert.isTrue(NetAddress.isPrivate(Schema.decodeUnknownSync(Schema.Ipv4PrivateAddressFromString)("10.0.0.1")))
802 assert.isTrue(
803 NetAddress.isUniqueLocal(Schema.decodeUnknownSync(Schema.Ipv6UniqueLocalAddressFromString)("fd00::1"))
804 )
805 assert.isTrue(
806 NetAddress.isMacLocallyAdministered(
807 Schema.decodeUnknownSync(Schema.MacLocallyAdministeredAddressFromString)("02:00:00:00:00:01")
808 )
809 )
810 assert.isTrue(
811 NetAddress.isMacUniversallyAdministered(
812 Schema.decodeUnknownSync(Schema.MacUniversallyAdministeredAddressFromString)("00:00:5e:00:53:01")
813 )
814 )
815
816 const loopback = NetAddress.ipv4Loopback
817 const decodedLoopback: NetAddress.IpAddress = Schema.decodeUnknownSync(Schema.IpLoopbackAddress)(loopback)
818 assert.strictEqual(decodedLoopback, loopback)
819 assert.isTrue(Schema.is(Schema.IpLoopbackAddress)(loopback))
820 assert.isFalse(Schema.is(Schema.IpMulticastAddress)(loopback))
821 assert.throws(
822 () => Schema.decodeUnknownSync(Schema.IpMulticastAddressFromString)("127.0.0.1"),
823 /Expected a multicast address/
824 )
825 assert.throws(
826 () => Schema.decodeUnknownSync(Schema.IpMulticastAddressFromString)("localhost"),
827 /expected exactly four decimal octets/
828 )
829 assert.throws(
830 () => Schema.encodeUnknownSync(Schema.IpMulticastAddressFromString)(loopback),
831 /Expected a multicast address/
832 )
833 })
834})