| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { assertTrue } from "@effect/vitest/utils" |
| 3 | import { Equal, Hash, Option, Result, Schema } from "effect" |
| 4 | import * as NetAddress from "effect/net/NetAddress" |
| 5 | import { Buffer } from "node:buffer" |
| 6 | import { inspect } from "node:util" |
| 7 | |
| 8 | const success = <A>(result: Result.Result<A, unknown>): A => { |
| 9 | assertTrue(Result.isSuccess(result), "expected Success") |
| 10 | return result.success |
| 11 | } |
| 12 | |
| 13 | const failure = <E>(result: Result.Result<unknown, E>): E => { |
| 14 | assertTrue(Result.isFailure(result), "expected Failure") |
| 15 | return result.failure |
| 16 | } |
| 17 | |
| 18 | describe("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 | }) |