1/**
2 * Pure, platform-neutral values for MAC, IP, internet socket, and Unix path addresses.
3 *
4 * @stability unstable
5 * @since 4.0.0
6 */
7import type * as Brand from "../Brand.ts"
8import * as Data from "../Data.ts"
9import * as Equal from "../Equal.ts"
10import { dual } from "../Function.ts"
11import * as Hash from "../Hash.ts"
12import { NodeInspectSymbol } from "../Inspectable.ts"
13import * as Option from "../Option.ts"
14import { hasProperty } from "../Predicate.ts"
15import * as Result from "../Result.ts"
16
17const TypeId = "~effect/net/NetAddress" as const
18
19/**
20 * An immutable 32-bit IPv4 address.
21 *
22 * @stability unstable
23 * @category models
24 * @since 4.0.0
25 */
26export interface Ipv4Address extends Equal.Equal, Hash.Hash {
27 readonly _tag: "Ipv4Address"
28 readonly [TypeId]: typeof TypeId
29 toString(): string
30 toJSON(): string
31}
32
33/**
34 * An immutable 128-bit IPv6 address without socket scope metadata.
35 *
36 * @stability unstable
37 * @category models
38 * @since 4.0.0
39 */
40export interface Ipv6Address extends Equal.Equal, Hash.Hash {
41 readonly _tag: "Ipv6Address"
42 readonly [TypeId]: typeof TypeId
43 toString(): string
44 toJSON(): string
45}
46
47/**
48 * A numeric IPv4 or IPv6 address.
49 *
50 * @stability unstable
51 * @category models
52 * @since 4.0.0
53 */
54export type IpAddress = Ipv4Address | Ipv6Address
55
56/**
57 * An immutable 48-bit IEEE 802 MAC address.
58 *
59 * @stability unstable
60 * @category models
61 * @since 4.0.0
62 */
63export interface MacAddress extends Equal.Equal, Hash.Hash {
64 readonly _tag: "MacAddress"
65 readonly [TypeId]: typeof TypeId
66 toString(): string
67 toJSON(): string
68}
69
70const MulticastTypeId = "~effect/net/NetAddress/MulticastAddress" as const
71const UnicastTypeId = "~effect/net/NetAddress/UnicastAddress" as const
72const BroadcastTypeId = "~effect/net/NetAddress/BroadcastAddress" as const
73const LoopbackTypeId = "~effect/net/NetAddress/LoopbackAddress" as const
74const LinkLocalTypeId = "~effect/net/NetAddress/LinkLocalAddress" as const
75const UnspecifiedTypeId = "~effect/net/NetAddress/UnspecifiedAddress" as const
76const PrivateTypeId = "~effect/net/NetAddress/PrivateAddress" as const
77const UniqueLocalTypeId = "~effect/net/NetAddress/UniqueLocalAddress" as const
78const LocallyAdministeredTypeId = "~effect/net/NetAddress/LocallyAdministeredAddress" as const
79const UniversallyAdministeredTypeId = "~effect/net/NetAddress/UniversallyAdministeredAddress" as const
80
81/**
82 * An IP or MAC address proven to be multicast (IPv4 `224.0.0.0/4`, IPv6
83 * `ff00::/8`, or a MAC address with the IEEE group bit set).
84 *
85 * **Details**
86 *
87 * This is a branded refinement of the underlying address. Construction returns
88 * the same runtime value, preserving identity, equality, hashing, and formatting.
89 * The MAC all-ones broadcast address has the group bit set and is therefore
90 * multicast; use {@link isMacBroadcast} to distinguish it. The IPv4 limited
91 * broadcast address `255.255.255.255` is not multicast.
92 *
93 * The default type argument remains `IpAddress`; specify `MacAddress` or an
94 * explicit IP/MAC union when accepting those wider address families.
95 *
96 * @stability unstable
97 * @category models
98 * @since 4.0.0
99 */
100export type MulticastAddress<A extends IpAddress | MacAddress = IpAddress> = Brand.Branded<
101 A,
102 typeof MulticastTypeId
103>
104
105/**
106 * An IP or MAC address proven to be syntactically unicast.
107 *
108 * **Details**
109 *
110 * For IP addresses this excludes multicast, unspecified, and the IPv4 limited
111 * broadcast address. Reserved and special-purpose addresses may still satisfy
112 * this refinement, as may an IPv4 directed broadcast whose network prefix is
113 * unknown. It does not promise reachability or destination usability. For MAC
114 * addresses it means that the IEEE individual/group bit is clear.
115 *
116 * @stability unstable
117 * @category models
118 * @since 4.0.0
119 */
120export type UnicastAddress<A extends IpAddress | MacAddress = IpAddress | MacAddress> = Brand.Branded<
121 A,
122 typeof UnicastTypeId
123>
124
125/**
126 * An IPv4 limited-broadcast or MAC all-ones broadcast address.
127 *
128 * **Details**
129 *
130 * MAC broadcast is also multicast because its IEEE group bit is set. IPv4
131 * limited broadcast is not multicast. Directed IPv4 broadcast requires network
132 * prefix context and is intentionally not represented by this refinement.
133 *
134 * @stability unstable
135 * @category models
136 * @since 4.0.0
137 */
138export type BroadcastAddress<A extends Ipv4Address | MacAddress = Ipv4Address | MacAddress> = A extends MacAddress
139 ? Brand.Branded<MulticastAddress<A>, typeof BroadcastTypeId>
140 : Brand.Branded<A, typeof BroadcastTypeId>
141
142/**
143 * An IP address proven to be loopback.
144 *
145 * @stability unstable
146 * @category models
147 * @since 4.0.0
148 */
149export type LoopbackAddress<A extends IpAddress = IpAddress> = Brand.Branded<A, typeof LoopbackTypeId>
150
151/**
152 * An IP address proven to be link-local.
153 *
154 * @stability unstable
155 * @category models
156 * @since 4.0.0
157 */
158export type LinkLocalAddress<A extends IpAddress = IpAddress> = Brand.Branded<A, typeof LinkLocalTypeId>
159
160/**
161 * An all-zero IPv4 or IPv6 address.
162 *
163 * @stability unstable
164 * @category models
165 * @since 4.0.0
166 */
167export type UnspecifiedAddress<A extends IpAddress = IpAddress> = Brand.Branded<A, typeof UnspecifiedTypeId>
168
169/**
170 * An IPv4 private-use address in an RFC 1918 range.
171 *
172 * @stability unstable
173 * @category models
174 * @since 4.0.0
175 */
176export type PrivateAddress<A extends Ipv4Address = Ipv4Address> = Brand.Branded<A, typeof PrivateTypeId>
177
178/**
179 * An IPv6 unique-local address in `fc00::/7`.
180 *
181 * @stability unstable
182 * @category models
183 * @since 4.0.0
184 */
185export type UniqueLocalAddress<A extends Ipv6Address = Ipv6Address> = Brand.Branded<A, typeof UniqueLocalTypeId>
186
187/**
188 * A MAC address whose IEEE universal/local bit marks it as locally administered.
189 *
190 * @stability unstable
191 * @category models
192 * @since 4.0.0
193 */
194export type LocallyAdministeredAddress<A extends MacAddress = MacAddress> = Brand.Branded<
195 A,
196 typeof LocallyAdministeredTypeId
197>
198
199/**
200 * A MAC address whose IEEE universal/local bit marks it as universally administered.
201 *
202 * @stability unstable
203 * @category models
204 * @since 4.0.0
205 */
206export type UniversallyAdministeredAddress<A extends MacAddress = MacAddress> = Brand.Branded<
207 A,
208 typeof UniversallyAdministeredTypeId
209>
210
211// IPv4 addresses store one unsigned 32-bit number and IPv6 addresses store four
212// unsigned 32-bit words, most significant first. Allocating a typed array per
213// address is expensive on some runtimes, notably Deno.
214const ipv4Value = (self: Ipv4Address): number => (self as any).value
215interface Ipv6Words {
216 readonly w0: number
217 readonly w1: number
218 readonly w2: number
219 readonly w3: number
220}
221const ipv6Words = (self: Ipv6Address): Ipv6Words => self as any
222const getMacBytes = (self: MacAddress): Uint8Array => (self as any).bytes
223
224/**
225 * A resolved IPv4 internet address and port.
226 *
227 * @stability unstable
228 * @category models
229 * @since 4.0.0
230 */
231export interface InetAddressV4 extends Equal.Equal, Hash.Hash {
232 readonly _tag: "InetAddressV4"
233 readonly address: Ipv4Address
234 readonly port: number
235 readonly [TypeId]: typeof TypeId
236 toString(): string
237 toJSON(): string
238}
239
240/**
241 * A resolved IPv6 internet address, port, and scope identifier.
242 *
243 * @stability unstable
244 * @category models
245 * @since 4.0.0
246 */
247export interface InetAddressV6 extends Equal.Equal, Hash.Hash {
248 readonly _tag: "InetAddressV6"
249 readonly address: Ipv6Address
250 readonly port: number
251 readonly scopeId: number
252 readonly [TypeId]: typeof TypeId
253 toString(): string
254 toJSON(): string
255}
256
257/**
258 * A resolved IPv4 or IPv6 internet address and port.
259 *
260 * @stability unstable
261 * @category models
262 * @since 4.0.0
263 */
264export type InetAddress = InetAddressV4 | InetAddressV6
265
266/**
267 * An opaque Unix-domain filesystem socket path.
268 *
269 * @stability unstable
270 * @category models
271 * @since 4.0.0
272 */
273export interface UnixPathAddress extends Equal.Equal, Hash.Hash {
274 readonly _tag: "UnixPathAddress"
275 readonly path: string
276 readonly [TypeId]: typeof TypeId
277 toString(): string
278 toJSON(): string
279}
280
281/**
282 * A portable concrete internet or Unix-domain filesystem address.
283 *
284 * @stability unstable
285 * @category models
286 * @since 4.0.0
287 */
288export type SocketAddress = InetAddress | UnixPathAddress
289
290/**
291 * Companion types for constructing socket addresses.
292 *
293 * @stability unstable
294 * @since 4.0.0
295 */
296export declare namespace SocketAddress {
297 /**
298 * Input that can be converted to a concrete `SocketAddress` without hostname
299 * resolution.
300 *
301 * **Details**
302 *
303 * String inputs must use `IPv4:port` or `[IPv6]:port` notation. Address
304 * properties must be numeric IPv4 or IPv6 literals.
305 *
306 * @stability unstable
307 * @category models
308 * @since 4.0.0
309 */
310 export type Input =
311 | SocketAddress
312 | string
313 | { readonly address: IpAddress | string; readonly port: number }
314 | { readonly path: string }
315}
316
317/**
318 * The IPv4 or IPv6 family of an IP address or internet address type.
319 *
320 * **Details**
321 *
322 * Refinements such as `MulticastAddress<A>` are dropped; the result is the
323 * plain `Ipv4Address` or `Ipv6Address` for the family. Unions distribute, so
324 * `Family<IpAddress>` and `Family<InetAddress>` are both `IpAddress`.
325 *
326 * **Example** (Deriving the family of an internet address)
327 *
328 * ```ts import.meta.vitest
329 * import { NetAddress } from "effect/net"
330 *
331 * const family: NetAddress.Family<NetAddress.InetAddressV6> = NetAddress.ipv6Loopback
332 * ```
333 *
334 * @stability unstable
335 * @category models
336 * @since 4.0.0
337 */
338export type Family<A extends IpAddress | InetAddress> = A extends Ipv4Address | InetAddressV4 ? Ipv4Address
339 : Ipv6Address
340
341/**
342 * The internet address type for an IP address family.
343 *
344 * **Details**
345 *
346 * `Inet<Ipv4Address>` is `InetAddressV4` and `Inet<Ipv6Address>` is
347 * `InetAddressV6`. Refinements on `A` are dropped because the port-carrying
348 * types hold a plain family address. `Inet<IpAddress>` is `InetAddress`.
349 *
350 * **Example** (Selecting the internet address for a family)
351 *
352 * ```ts import.meta.vitest
353 * import { Result } from "effect"
354 * import { NetAddress } from "effect/net"
355 *
356 * const endpoint: NetAddress.Inet<NetAddress.Ipv4Address> = Result.getOrThrow(
357 * NetAddress.inetAddressV4(NetAddress.ipv4Loopback, 8080)
358 * )
359 * ```
360 *
361 * @stability unstable
362 * @category models
363 * @since 4.0.0
364 */
365export type Inet<A extends IpAddress = IpAddress> = A extends Ipv4Address ? InetAddressV4 : InetAddressV6
366
367/**
368 * A native multicast interface selector for an IP address family: an interface
369 * address for IPv4 or an interface index for IPv6. `ipv4Unspecified` and index
370 * `0` select the operating system default.
371 *
372 * **Example** (Selecting multicast interfaces by family)
373 *
374 * ```ts import.meta.vitest
375 * import { NetAddress } from "effect/net"
376 *
377 * const ipv4Interface: NetAddress.MulticastInterface<NetAddress.Ipv4Address> =
378 * NetAddress.ipv4Unspecified
379 * const ipv6Interface: NetAddress.MulticastInterface<NetAddress.Ipv6Address> = 0
380 * ```
381 *
382 * @see {@link formatMulticastInterface}
383 * @stability unstable
384 * @category models
385 * @since 4.0.0
386 */
387export type MulticastInterface<A extends IpAddress = IpAddress> = A extends Ipv4Address ? Ipv4Address : number
388
389/**
390 * A checked network-address operation failure retaining the address or supplied input.
391 *
392 * **Details**
393 *
394 * Address-based operations retain the address value. Parsing and numeric
395 * construction retain the supplied string or array. Composed operations forward
396 * errors from the failing operation unchanged. Failures from external operations
397 * retain the original exception in `cause` when available.
398 *
399 * @stability unstable
400 * @category errors
401 * @since 4.0.0
402 */
403export class NetAddressError extends Data.TaggedError("NetAddressError")<{
404 readonly message: string
405 readonly input: unknown
406 readonly cause?: unknown
407}> {}
408
409const isAddress = (u: unknown): u is IpAddress | MacAddress | SocketAddress => hasProperty(u, TypeId)
410
411/**
412 * Returns `true` when a value is an IPv4 address.
413 *
414 * @stability unstable
415 * @category guards
416 * @since 4.0.0
417 */
418export const isIpv4Address = (u: unknown): u is Ipv4Address => isAddress(u) && u._tag === "Ipv4Address"
419
420/**
421 * Returns `true` when a value is an IPv6 address.
422 *
423 * @stability unstable
424 * @category guards
425 * @since 4.0.0
426 */
427export const isIpv6Address = (u: unknown): u is Ipv6Address => isAddress(u) && u._tag === "Ipv6Address"
428
429/**
430 * Returns `true` when a value is an IPv4 or IPv6 address.
431 *
432 * @stability unstable
433 * @category guards
434 * @since 4.0.0
435 */
436export const isIpAddress = (u: unknown): u is IpAddress => isIpv4Address(u) || isIpv6Address(u)
437
438/**
439 * Returns the bit width of an IP address.
440 *
441 * @stability unstable
442 * @category getters
443 * @since 4.0.0
444 */
445export const width = (address: IpAddress): 32 | 128 => isIpv4Address(address) ? 32 : 128
446
447/**
448 * Returns `true` when a value is a MAC address.
449 *
450 * @stability unstable
451 * @category guards
452 * @since 4.0.0
453 */
454export const isMacAddress = (u: unknown): u is MacAddress => isAddress(u) && u._tag === "MacAddress"
455
456/**
457 * Returns `true` when a value is a resolved IPv4 internet address.
458 *
459 * @stability unstable
460 * @category guards
461 * @since 4.0.0
462 */
463export const isInetAddressV4 = (u: unknown): u is InetAddressV4 => isAddress(u) && u._tag === "InetAddressV4"
464
465/**
466 * Returns `true` when a value is a resolved IPv6 internet address.
467 *
468 * @stability unstable
469 * @category guards
470 * @since 4.0.0
471 */
472export const isInetAddressV6 = (u: unknown): u is InetAddressV6 => isAddress(u) && u._tag === "InetAddressV6"
473
474/**
475 * Returns `true` when a value is a resolved internet address.
476 *
477 * @stability unstable
478 * @category guards
479 * @since 4.0.0
480 */
481export const isInetAddress = (u: unknown): u is InetAddress => isInetAddressV4(u) || isInetAddressV6(u)
482
483/**
484 * Returns `true` when a value is a Unix-domain filesystem address.
485 *
486 * @stability unstable
487 * @category guards
488 * @since 4.0.0
489 */
490export const isUnixPathAddress = (u: unknown): u is UnixPathAddress => isAddress(u) && u._tag === "UnixPathAddress"
491
492/**
493 * Returns `true` when a value is a portable concrete socket address.
494 *
495 * @stability unstable
496 * @category guards
497 * @since 4.0.0
498 */
499export const isSocketAddress = (u: unknown): u is SocketAddress => isInetAddress(u) || isUnixPathAddress(u)
500
501const Ipv4Proto = {
502 _tag: "Ipv4Address",
503 [TypeId]: TypeId,
504 [Equal.symbol](this: Ipv4Address, that: Equal.Equal): boolean {
505 return isIpv4Address(that) && ipv4Value(this) === ipv4Value(that)
506 },
507 [Hash.symbol](this: Ipv4Address): number {
508 return Hash.optimize(Hash.combine(ipv4HashSeed, ipv4Value(this) | 0))
509 },
510 toString(this: Ipv4Address): string {
511 return formatIp(this)
512 },
513 toJSON(this: Ipv4Address): string {
514 return this.toString()
515 },
516 [NodeInspectSymbol](this: Ipv4Address): string {
517 return this.toJSON()
518 }
519}
520
521const Ipv6Proto = {
522 _tag: "Ipv6Address",
523 [TypeId]: TypeId,
524 [Equal.symbol](this: Ipv6Address, that: Equal.Equal): boolean {
525 if (!isIpv6Address(that)) return false
526 const self = ipv6Words(this)
527 const other = ipv6Words(that)
528 return self.w0 === other.w0 && self.w1 === other.w1 && self.w2 === other.w2 && self.w3 === other.w3
529 },
530 [Hash.symbol](this: Ipv6Address): number {
531 const self = ipv6Words(this)
532 let h = Hash.combine(ipv6HashSeed, self.w0 | 0)
533 h = Hash.combine(h, self.w1 | 0)
534 h = Hash.combine(h, self.w2 | 0)
535 return Hash.optimize(Hash.combine(h, self.w3 | 0))
536 },
537 toString(this: Ipv6Address): string {
538 return formatIp(this)
539 },
540 toJSON(this: Ipv6Address): string {
541 return this.toString()
542 },
543 [NodeInspectSymbol](this: Ipv6Address): string {
544 return this.toJSON()
545 }
546}
547
548const MacProto = {
549 _tag: "MacAddress",
550 [TypeId]: TypeId,
551 [Equal.symbol](this: MacAddress, that: Equal.Equal): boolean {
552 return isMacAddress(that) && bytesEqual(getMacBytes(this), getMacBytes(that))
553 },
554 [Hash.symbol](this: MacAddress): number {
555 return Hash.combine(macHashSeed, Hash.array(getMacBytes(this)))
556 },
557 toString(this: MacAddress): string {
558 return formatMacAddress(this)
559 },
560 toJSON(this: MacAddress): string {
561 return this.toString()
562 },
563 [NodeInspectSymbol](this: MacAddress): string {
564 return this.toJSON()
565 }
566}
567
568const bytesEqual = (self: Uint8Array, that: Uint8Array): boolean => {
569 if (self.length !== that.length) return false
570 for (let index = 0; index < self.length; index++) {
571 if (self[index] !== that[index]) return false
572 }
573 return true
574}
575
576const ipv4HashSeed = Hash.string("Ipv4Address")
577const ipv6HashSeed = Hash.string("Ipv6Address")
578const macHashSeed = Hash.string("MacAddress")
579
580const makeIpv4 = (value: number): Ipv4Address => {
581 const self = Object.create(Ipv4Proto)
582 self.value = value >>> 0
583 return Object.freeze(self)
584}
585
586const makeIpv6 = (w0: number, w1: number, w2: number, w3: number): Ipv6Address => {
587 const self = Object.create(Ipv6Proto)
588 self.w0 = w0 >>> 0
589 self.w1 = w1 >>> 0
590 self.w2 = w2 >>> 0
591 self.w3 = w3 >>> 0
592 return Object.freeze(self)
593}
594
595const packOctets = (a: number, b: number, c: number, d: number): number => ((a << 24) | (b << 16) | (c << 8) | d) >>> 0
596
597const packBytes = (bytes: Uint8Array, offset: number): number =>
598 packOctets(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3])
599
600const wordToOctets = (word: number): readonly [number, number, number, number] => [
601 word >>> 24,
602 (word >>> 16) & 0xff,
603 (word >>> 8) & 0xff,
604 word & 0xff
605]
606
607const makeMac = (bytes: Uint8Array): MacAddress => {
608 const self = Object.assign(Object.create(MacProto), { bytes: new Uint8Array(bytes) })
609 return Object.freeze(self)
610}
611
612/**
613 * Creates an IPv4 address from trusted network-order bytes without validation.
614 *
615 * @stability unstable
616 * @category unsafe
617 * @since 4.0.0
618 */
619export const ipv4FromBytesUnsafe = (bytes: Uint8Array): Ipv4Address => makeIpv4(packBytes(bytes, 0))
620
621/**
622 * Creates an IPv6 address from trusted network-order bytes without validation.
623 *
624 * @stability unstable
625 * @category unsafe
626 * @since 4.0.0
627 */
628export const ipv6FromBytesUnsafe = (bytes: Uint8Array): Ipv6Address =>
629 makeIpv6(packBytes(bytes, 0), packBytes(bytes, 4), packBytes(bytes, 8), packBytes(bytes, 12))
630
631/**
632 * The IPv4 loopback address `127.0.0.1`.
633 *
634 * @stability unstable
635 * @category constants
636 * @since 4.0.0
637 */
638export const ipv4Loopback: Ipv4Address = makeIpv4(0x7f000001)
639
640/**
641 * The IPv6 loopback address `::1`.
642 *
643 * @stability unstable
644 * @category constants
645 * @since 4.0.0
646 */
647export const ipv6Loopback: Ipv6Address = makeIpv6(0, 0, 0, 1)
648
649/**
650 * The unspecified IPv4 address `0.0.0.0`.
651 *
652 * @stability unstable
653 * @category constants
654 * @since 4.0.0
655 */
656export const ipv4Unspecified: Ipv4Address = makeIpv4(0)
657
658/**
659 * The unspecified IPv6 address `::`.
660 *
661 * @stability unstable
662 * @category constants
663 * @since 4.0.0
664 */
665export const ipv6Unspecified: Ipv6Address = makeIpv6(0, 0, 0, 0)
666
667/**
668 * The IPv4 broadcast address `255.255.255.255`.
669 *
670 * @stability unstable
671 * @category constants
672 * @since 4.0.0
673 */
674export const ipv4Broadcast: Ipv4Address = makeIpv4(0xffffffff)
675
676const addressError = (input: unknown, message: string): Result.Result<never, NetAddressError> =>
677 Result.fail(new NetAddressError({ input, message }))
678
679/**
680 * Creates an IPv4 address from four checked octets.
681 *
682 * @stability unstable
683 * @category constructors
684 * @since 4.0.0
685 */
686export const ipv4FromOctets = (
687 octets: readonly [number, number, number, number]
688): Result.Result<Ipv4Address, NetAddressError> => {
689 if (!octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)) {
690 return addressError(octets, "octets must be integers from 0 through 255")
691 }
692 return Result.succeed(makeIpv4(packOctets(octets[0], octets[1], octets[2], octets[3])))
693}
694
695/**
696 * Creates an IPv6 address from eight checked 16-bit segments.
697 *
698 * @stability unstable
699 * @category constructors
700 * @since 4.0.0
701 */
702export const ipv6FromSegments = (
703 segments: readonly [number, number, number, number, number, number, number, number]
704): Result.Result<Ipv6Address, NetAddressError> => {
705 if (!segments.every((n) => Number.isInteger(n) && n >= 0 && n <= 0xffff)) {
706 return addressError(segments, "segments must be integers from 0 through 65535")
707 }
708 return Result.succeed(makeIpv6(
709 segments[0] * 0x10000 + segments[1],
710 segments[2] * 0x10000 + segments[3],
711 segments[4] * 0x10000 + segments[5],
712 segments[6] * 0x10000 + segments[7]
713 ))
714}
715
716/**
717 * Creates a MAC address from six checked octets.
718 *
719 * @stability unstable
720 * @category constructors
721 * @since 4.0.0
722 */
723export const macAddressFromOctets = (
724 octets: readonly [number, number, number, number, number, number]
725): Result.Result<MacAddress, NetAddressError> => {
726 if (!octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)) {
727 return addressError(octets, "octets must be integers from 0 through 255")
728 }
729 return Result.succeed(makeMac(new Uint8Array(octets)))
730}
731
732/**
733 * Parses a colon-separated MAC address containing six two-digit hexadecimal octets.
734 *
735 * @stability unstable
736 * @category decoding
737 * @since 4.0.0
738 */
739export const macAddressFromString = (input: string): Result.Result<MacAddress, NetAddressError> => {
740 if (!/^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/.test(input)) {
741 return addressError(input, "expected six two-digit hexadecimal octets separated by colons")
742 }
743 return Result.succeed(makeMac(new Uint8Array(input.split(":").map((part) => Number.parseInt(part, 16)))))
744}
745
746/**
747 * Parses a trusted colon-separated MAC address, throwing on failure.
748 *
749 * @stability unstable
750 * @category unsafe
751 * @since 4.0.0
752 */
753export const macAddressFromStringUnsafe = (input: string): MacAddress => Result.getOrThrow(macAddressFromString(input))
754
755/**
756 * Parses a strict dotted-decimal IPv4 address.
757 *
758 * **Details**
759 *
760 * Multi-digit octets with a leading zero are rejected to avoid octal ambiguity.
761 *
762 * @stability unstable
763 * @category decoding
764 * @since 4.0.0
765 */
766export const ipv4FromString = (input: string): Result.Result<Ipv4Address, NetAddressError> => {
767 const parts = input.split(".")
768 if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part))) {
769 return addressError(input, "expected exactly four decimal octets")
770 }
771 if (parts.some((part) => part.length > 1 && part[0] === "0")) {
772 return addressError(input, "leading zeroes are not allowed")
773 }
774 const octets = parts.map(Number)
775 if (octets.some((part) => part > 255)) {
776 return addressError(input, "octets must be at most 255")
777 }
778 return ipv4FromOctets([octets[0], octets[1], octets[2], octets[3]])
779}
780
781const parseIpv6Segments = (input: string): Result.Result<ReadonlyArray<number>, NetAddressError> => {
782 if (input.includes("[") || input.includes("]") || input.includes("%")) {
783 return addressError(input, "brackets and zone identifiers are not valid in a bare IPv6 address")
784 }
785 const halves = input.split("::")
786 if (halves.length > 2) return addressError(input, "only one compression marker is allowed")
787 const head = halves[0] === "" ? [] : halves[0].split(":")
788 const tail = halves.length === 2 && halves[1] !== "" ? halves[1].split(":") : []
789 if (head.some((part) => part === "") || tail.some((part) => part === "")) {
790 return addressError(input, "empty segments are only valid in the compression marker")
791 }
792 const trailing = tail.length > 0 ? tail[tail.length - 1] : head.length > 0 ? head[head.length - 1] : ""
793 let embedded: ReadonlyArray<number> | undefined
794 if (trailing.includes(".")) {
795 if (halves.length === 2 && tail.length === 0) {
796 return addressError(input, "embedded IPv4 syntax must be trailing")
797 }
798 const parsed = Result.map(ipv4FromString(trailing), (address) => {
799 const value = ipv4Value(address)
800 return [value >>> 16, value & 0xffff]
801 })
802 if (Result.isFailure(parsed)) return parsed
803 embedded = parsed.success
804 if (tail.length > 0) tail.pop()
805 else head.pop()
806 }
807 const explicit = head.length + tail.length + (embedded ? 2 : 0)
808 if (halves.length === 1 ? explicit !== 8 : explicit >= 8) {
809 return addressError(
810 input,
811 halves.length === 1 ? "expected eight segments" : "compression must replace at least one segment"
812 )
813 }
814 const parse = (part: string): number | undefined =>
815 /^[0-9a-fA-F]{1,4}$/.test(part) ? Number.parseInt(part, 16) : undefined
816 const parsedHead = head.map(parse)
817 const parsedTail = tail.map(parse)
818 if (parsedHead.some((part) => part === undefined) || parsedTail.some((part) => part === undefined)) {
819 return addressError(input, "segments must contain one through four hexadecimal digits")
820 }
821 return Result.succeed([
822 ...parsedHead as ReadonlyArray<number>,
823 ...Array(8 - explicit).fill(0),
824 ...parsedTail as ReadonlyArray<number>,
825 ...(embedded ?? [])
826 ])
827}
828
829/**
830 * Parses an IPv6 address with optional compression and trailing embedded IPv4.
831 *
832 * @stability unstable
833 * @category decoding
834 * @since 4.0.0
835 */
836export const ipv6FromString = (input: string): Result.Result<Ipv6Address, NetAddressError> => {
837 return Result.flatMap(
838 parseIpv6Segments(input),
839 (segments) =>
840 ipv6FromSegments(
841 segments as unknown as readonly [
842 number,
843 number,
844 number,
845 number,
846 number,
847 number,
848 number,
849 number
850 ]
851 )
852 )
853}
854
855/**
856 * Parses a bare numeric IPv4 or IPv6 address.
857 *
858 * @stability unstable
859 * @category decoding
860 * @since 4.0.0
861 */
862export const ipFromString = (input: string): Result.Result<IpAddress, NetAddressError> => {
863 const result: Result.Result<IpAddress, NetAddressError> = input.includes(":")
864 ? ipv6FromString(input)
865 : ipv4FromString(input)
866 return result
867}
868
869/**
870 * Parses a trusted bare numeric IPv4 or IPv6 address, throwing on failure.
871 *
872 * @stability unstable
873 * @category unsafe
874 * @since 4.0.0
875 */
876export const ipFromStringUnsafe = (input: string): IpAddress => Result.getOrThrow(ipFromString(input))
877
878/**
879 * Returns the four numeric octets of an IPv4 address in a fresh tuple.
880 *
881 * @stability unstable
882 * @category getters
883 * @since 4.0.0
884 */
885export const ipv4ToOctets = (self: Ipv4Address): readonly [number, number, number, number] =>
886 wordToOctets(ipv4Value(self))
887
888/**
889 * Returns the eight numeric segments of an IPv6 address in a fresh tuple.
890 *
891 * @stability unstable
892 * @category getters
893 * @since 4.0.0
894 */
895export const ipv6ToSegments = (
896 self: Ipv6Address
897): readonly [number, number, number, number, number, number, number, number] => {
898 const { w0, w1, w2, w3 } = ipv6Words(self)
899 return [w0 >>> 16, w0 & 0xffff, w1 >>> 16, w1 & 0xffff, w2 >>> 16, w2 & 0xffff, w3 >>> 16, w3 & 0xffff]
900}
901
902/**
903 * Returns the sixteen numeric octets of an IPv6 address in a fresh array.
904 *
905 * @stability unstable
906 * @category getters
907 * @since 4.0.0
908 */
909export const ipv6ToOctets = (
910 self: Ipv6Address
911): ReadonlyArray<number> => {
912 const { w0, w1, w2, w3 } = ipv6Words(self)
913 return [...wordToOctets(w0), ...wordToOctets(w1), ...wordToOctets(w2), ...wordToOctets(w3)]
914}
915
916/**
917 * Returns the six numeric octets of a MAC address in a fresh tuple.
918 *
919 * @stability unstable
920 * @category getters
921 * @since 4.0.0
922 */
923export const macAddressToOctets = (
924 self: MacAddress
925): readonly [number, number, number, number, number, number] => {
926 const bytes = getMacBytes(self)
927 return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]]
928}
929
930/**
931 * Formats a MAC address as six lowercase hexadecimal octets separated by colons.
932 *
933 * @stability unstable
934 * @category encoding
935 * @since 4.0.0
936 */
937export const formatMacAddress = (self: MacAddress): string =>
938 Array.from(getMacBytes(self), (byte) => byte.toString(16).padStart(2, "0")).join(":")
939
940/**
941 * Returns `true` when the MAC address is the all-ones broadcast address.
942 *
943 * @stability unstable
944 * @category predicates
945 * @since 4.0.0
946 */
947export const isMacBroadcast = <A extends MacAddress>(self: A): self is BroadcastAddress<A> =>
948 getMacBytes(self).every((byte) => byte === 0xff)
949
950/**
951 * Returns `true` when the MAC address has the IEEE group-address bit set.
952 *
953 * @stability unstable
954 * @category predicates
955 * @since 4.0.0
956 */
957export const isMacMulticast = <A extends MacAddress>(self: A): self is MulticastAddress<A> =>
958 (getMacBytes(self)[0] & 1) !== 0
959
960/**
961 * Returns `true` when the MAC address has the IEEE group-address bit clear.
962 *
963 * @stability unstable
964 * @category predicates
965 * @since 4.0.0
966 */
967export const isMacUnicast = <A extends MacAddress>(self: A): self is UnicastAddress<A> =>
968 (getMacBytes(self)[0] & 1) === 0
969
970/**
971 * Returns `true` when the MAC address has the IEEE local-administration bit set.
972 *
973 * @stability unstable
974 * @category predicates
975 * @since 4.0.0
976 */
977export const isMacLocallyAdministered = <A extends MacAddress>(
978 self: A
979): self is LocallyAdministeredAddress<A> => (getMacBytes(self)[0] & 2) !== 0
980
981/**
982 * Returns `true` when the MAC address has the IEEE local-administration bit clear.
983 *
984 * @stability unstable
985 * @category predicates
986 * @since 4.0.0
987 */
988export const isMacUniversallyAdministered = <A extends MacAddress>(
989 self: A
990): self is UniversallyAdministeredAddress<A> => (getMacBytes(self)[0] & 2) === 0
991
992/**
993 * Folds an IP address by its numeric version.
994 *
995 * @stability unstable
996 * @category pattern matching
997 * @since 4.0.0
998 */
999export const match: {
1000 <A, B>(options: {
1001 readonly onIpv4: (address: Ipv4Address) => A
1002 readonly onIpv6: (address: Ipv6Address) => B
1003 }): (self: IpAddress) => A | B
1004 <A, B>(self: IpAddress, options: {
1005 readonly onIpv4: (address: Ipv4Address) => A
1006 readonly onIpv6: (address: Ipv6Address) => B
1007 }): A | B
1008} = dual(2, <A, B>(self: IpAddress, options: {
1009 readonly onIpv4: (address: Ipv4Address) => A
1010 readonly onIpv6: (address: Ipv6Address) => B
1011}): A | B => isIpv4Address(self) ? options.onIpv4(self) : options.onIpv6(self))
1012
1013/**
1014 * Formats an IP address in canonical numeric form.
1015 *
1016 * @stability unstable
1017 * @category encoding
1018 * @since 4.0.0
1019 */
1020export const formatIp = (self: IpAddress): string => {
1021 if (isIpv4Address(self)) return ipv4ToOctets(self).join(".")
1022 const segments = ipv6ToSegments(self)
1023 if (isIpv4Mapped(self)) {
1024 return `::ffff:${segments[6] >> 8}.${segments[6] & 0xff}.${segments[7] >> 8}.${segments[7] & 0xff}`
1025 }
1026 let bestStart = -1
1027 let bestLength = 0
1028 let start = -1
1029 for (let index = 0; index <= 8; index++) {
1030 if (index < 8 && segments[index] === 0) {
1031 if (start === -1) start = index
1032 } else if (start !== -1) {
1033 if (index - start > bestLength) {
1034 bestStart = start
1035 bestLength = index - start
1036 }
1037 start = -1
1038 }
1039 }
1040 if (bestLength < 2) return segments.map((segment) => segment.toString(16)).join(":")
1041 const head = segments.slice(0, bestStart).map((segment) => segment.toString(16)).join(":")
1042 const tail = segments.slice(bestStart + bestLength).map((segment) => segment.toString(16)).join(":")
1043 return `${head}::${tail}`
1044}
1045
1046/**
1047 * Returns `true` for the all-zero address of either IP version.
1048 *
1049 * @stability unstable
1050 * @category predicates
1051 * @since 4.0.0
1052 */
1053export const isUnspecified = <A extends IpAddress>(self: A): self is UnspecifiedAddress<A> => {
1054 if (isIpv4Address(self)) return ipv4Value(self) === 0
1055 const { w0, w1, w2, w3 } = ipv6Words(self)
1056 return w0 === 0 && w1 === 0 && w2 === 0 && w3 === 0
1057}
1058
1059/**
1060 * Returns `true` for IPv4 `127.0.0.0/8` or IPv6 `::1`.
1061 *
1062 * @stability unstable
1063 * @category predicates
1064 * @since 4.0.0
1065 */
1066export const isLoopback = <A extends IpAddress>(self: A): self is LoopbackAddress<A> => {
1067 if (isIpv4Address(self)) return (ipv4Value(self) >>> 24) === 127
1068 const { w0, w1, w2, w3 } = ipv6Words(self)
1069 return w0 === 0 && w1 === 0 && w2 === 0 && w3 === 1
1070}
1071
1072/**
1073 * Returns `true` for IPv4 `224.0.0.0/4`, IPv6 `ff00::/8`, or a MAC address with
1074 * the IEEE group bit set, refining the value while preserving its address type.
1075 *
1076 * **Details**
1077 *
1078 * The MAC all-ones broadcast address has the group bit set and is multicast;
1079 * the IPv4 limited broadcast address `255.255.255.255` is not. Use
1080 * {@link isMacBroadcast} or {@link isBroadcast} to distinguish broadcast.
1081 *
1082 * **Example** (Classifying IP and MAC group addresses)
1083 *
1084 * ```ts import.meta.vitest
1085 * import { assert } from "@effect/vitest"
1086 * import { NetAddress } from "effect/net"
1087 *
1088 * const ip = NetAddress.ipFromStringUnsafe("239.255.0.1")
1089 * const mac = NetAddress.macAddressFromStringUnsafe("ff:ff:ff:ff:ff:ff")
1090 *
1091 * assert.isTrue(NetAddress.isMulticast(ip))
1092 * assert.isTrue(NetAddress.isMulticast(mac))
1093 * assert.isTrue(NetAddress.isBroadcast(mac))
1094 * assert.isFalse(NetAddress.isMulticast(NetAddress.ipv4Broadcast))
1095 * ```
1096 *
1097 * @stability unstable
1098 * @category predicates
1099 * @since 4.0.0
1100 */
1101export const isMulticast = <A extends IpAddress | MacAddress>(self: A): self is MulticastAddress<A> => {
1102 if (isMacAddress(self)) return isMacMulticast(self)
1103 if (isIpv4Address(self)) return (ipv4Value(self) >>> 28) === 0xe
1104 return (ipv6Words(self).w0 >>> 24) === 0xff
1105}
1106
1107/**
1108 * Returns `true` for the IPv4 limited broadcast address `255.255.255.255` or
1109 * the MAC all-ones broadcast address.
1110 *
1111 * **Details**
1112 *
1113 * MAC broadcast is also multicast because its IEEE group bit is set. IPv4
1114 * limited broadcast is not multicast. Directed IPv4 broadcast cannot be
1115 * determined without a network prefix and is not classified here.
1116 *
1117 * @stability unstable
1118 * @category predicates
1119 * @since 4.0.0
1120 */
1121export const isBroadcast = <A extends Ipv4Address | MacAddress>(self: A): self is BroadcastAddress<A> =>
1122 isMacAddress(self) ? isMacBroadcast(self) : ipv4Value(self) === 0xffffffff
1123
1124/**
1125 * Returns `true` when an address is syntactically unicast.
1126 *
1127 * **Details**
1128 *
1129 * IP addresses exclude multicast, unspecified, and IPv4 limited broadcast.
1130 * This does not exclude reserved ranges, prefix-relative directed broadcast,
1131 * or addresses that are unusable or unreachable in a particular deployment.
1132 * MAC addresses are unicast when their IEEE individual/group bit is clear.
1133 *
1134 * @stability unstable
1135 * @category predicates
1136 * @since 4.0.0
1137 */
1138export const isUnicast = <A extends IpAddress | MacAddress>(self: A): self is UnicastAddress<A> => {
1139 if (isMacAddress(self)) return isMacUnicast(self)
1140 return !isMulticast(self) && !isUnspecified(self) && (!isIpv4Address(self) || !isBroadcast(self))
1141}
1142
1143/**
1144 * Returns `true` for IPv4 `169.254.0.0/16` or IPv6 `fe80::/10`.
1145 *
1146 * @stability unstable
1147 * @category predicates
1148 * @since 4.0.0
1149 */
1150export const isLinkLocal = <A extends IpAddress>(self: A): self is LinkLocalAddress<A> => {
1151 if (isIpv4Address(self)) return (ipv4Value(self) >>> 16) === 0xa9fe
1152 return ((ipv6Words(self).w0 >>> 16) & 0xffc0) === 0xfe80
1153}
1154
1155/**
1156 * Returns `true` for IPv4 private-use ranges defined by RFC 1918.
1157 *
1158 * @stability unstable
1159 * @category predicates
1160 * @since 4.0.0
1161 */
1162export const isPrivate = <A extends Ipv4Address>(self: A): self is PrivateAddress<A> => {
1163 const value = ipv4Value(self)
1164 return (value >>> 24) === 10 || ((value >>> 16) & 0xfff0) === 0xac10 || (value >>> 16) === 0xc0a8
1165}
1166
1167/**
1168 * Returns `true` for IPv6 unique-local addresses in `fc00::/7`.
1169 *
1170 * @stability unstable
1171 * @category predicates
1172 * @since 4.0.0
1173 */
1174export const isUniqueLocal = <A extends Ipv6Address>(self: A): self is UniqueLocalAddress<A> =>
1175 ((ipv6Words(self).w0 >>> 24) & 0xfe) === 0xfc
1176
1177/**
1178 * Returns `true` when an IPv6 address is in the `::ffff:0:0/96` mapped range.
1179 *
1180 * @stability unstable
1181 * @category predicates
1182 * @since 4.0.0
1183 */
1184export const isIpv4Mapped = (self: Ipv6Address): boolean => {
1185 const { w0, w1, w2 } = ipv6Words(self)
1186 return w0 === 0 && w1 === 0 && w2 === 0xffff
1187}
1188
1189/**
1190 * Converts an IPv4 address to its IPv4-mapped IPv6 representation.
1191 *
1192 * @stability unstable
1193 * @category converting
1194 * @since 4.0.0
1195 */
1196export const toIpv4Mapped = (self: Ipv4Address): Ipv6Address => makeIpv6(0, 0, 0xffff, ipv4Value(self))
1197
1198/**
1199 * Extracts the IPv4 value from an IPv4-mapped IPv6 address.
1200 *
1201 * @stability unstable
1202 * @category converting
1203 * @since 4.0.0
1204 */
1205export const fromIpv4Mapped = (self: Ipv6Address): Option.Option<Ipv4Address> =>
1206 isIpv4Mapped(self) ? Option.some(makeIpv4(ipv6Words(self).w3)) : Option.none()
1207
1208/**
1209 * Converts IPv4-mapped IPv6 addresses to IPv4, including the IP component of internet addresses.
1210 *
1211 * **Details**
1212 *
1213 * Internet addresses retain their port. Addresses that need no conversion are
1214 * returned unchanged, preserving their identity and any IPv6 scope identifier.
1215 * Converted internet addresses are IPv4 values without IPv6 scope metadata.
1216 *
1217 * @stability unstable
1218 * @category converting
1219 * @since 4.0.0
1220 */
1221export function toCanonical(self: IpAddress): IpAddress
1222/**
1223 * @stability unstable
1224 */
1225export function toCanonical(self: InetAddress): InetAddress
1226/**
1227 * @stability unstable
1228 */
1229export function toCanonical(self: IpAddress | InetAddress): IpAddress | InetAddress {
1230 if (isInetAddress(self)) {
1231 const address = toCanonical(self.address)
1232 return address === self.address ? self : inetAddressUnsafe(address, self.port)
1233 }
1234 return isIpv6Address(self) ? Option.getOrElse(fromIpv4Mapped(self), () => self) : self
1235}
1236
1237const InetV4Proto = {
1238 _tag: "InetAddressV4",
1239 [TypeId]: TypeId,
1240 [Equal.symbol](this: InetAddressV4, that: Equal.Equal): boolean {
1241 return isInetAddressV4(that) && this.port === that.port && Equal.equals(this.address, that.address)
1242 },
1243 [Hash.symbol](this: InetAddressV4): number {
1244 return Hash.combine(Hash.hash(this.address), Hash.number(this.port))
1245 },
1246 toString(this: InetAddressV4): string {
1247 return formatInet(this)
1248 },
1249 toJSON(this: InetAddressV4): string {
1250 return this.toString()
1251 },
1252 [NodeInspectSymbol](this: InetAddressV4): string {
1253 return this.toJSON()
1254 }
1255}
1256
1257const InetV6Proto = {
1258 _tag: "InetAddressV6",
1259 [TypeId]: TypeId,
1260 [Equal.symbol](this: InetAddressV6, that: Equal.Equal): boolean {
1261 return isInetAddressV6(that) && this.port === that.port && this.scopeId === that.scopeId &&
1262 Equal.equals(this.address, that.address)
1263 },
1264 [Hash.symbol](this: InetAddressV6): number {
1265 return Hash.combine(
1266 Hash.combine(Hash.hash(this.address), Hash.number(this.port)),
1267 Hash.number(this.scopeId)
1268 )
1269 },
1270 toString(this: InetAddressV6): string {
1271 return formatInet(this)
1272 },
1273 toJSON(this: InetAddressV6): string {
1274 return this.toString()
1275 },
1276 [NodeInspectSymbol](this: InetAddressV6): string {
1277 return this.toJSON()
1278 }
1279}
1280
1281const checkPort = (port: number): boolean => Number.isInteger(port) && port >= 0 && port <= 0xffff
1282
1283/**
1284 * Creates a checked IPv4 internet address.
1285 *
1286 * @stability unstable
1287 * @category constructors
1288 * @since 4.0.0
1289 */
1290export const inetAddressV4 = (address: Ipv4Address, port: number): Result.Result<InetAddressV4, NetAddressError> => {
1291 if (!checkPort(port)) {
1292 return addressError(address, "port must be an integer from 0 through 65535")
1293 }
1294 const self = Object.create(InetV4Proto)
1295 self.address = address
1296 self.port = port
1297 return Result.succeed(Object.freeze(self))
1298}
1299
1300/**
1301 * Creates a checked IPv6 internet address with an optional scope identifier.
1302 *
1303 * @stability unstable
1304 * @category constructors
1305 * @since 4.0.0
1306 */
1307export const inetAddressV6 = (
1308 address: Ipv6Address,
1309 port: number,
1310 options?: { readonly scopeId?: number | undefined }
1311): Result.Result<InetAddressV6, NetAddressError> => {
1312 if (!checkPort(port)) {
1313 return addressError(address, "port must be an integer from 0 through 65535")
1314 }
1315 const scopeId = options?.scopeId ?? 0
1316 if (!Number.isInteger(scopeId) || scopeId < 0 || scopeId > 0xffffffff) {
1317 return addressError(address, "scopeId must be an unsigned 32-bit integer")
1318 }
1319 const self = Object.create(InetV6Proto)
1320 self.address = address
1321 self.port = port
1322 self.scopeId = scopeId
1323 return Result.succeed(Object.freeze(self))
1324}
1325
1326/**
1327 * Creates a checked internet address for an IP address and port.
1328 *
1329 * @stability unstable
1330 * @category constructors
1331 * @since 4.0.0
1332 */
1333export const inetAddress = (address: IpAddress, port: number): Result.Result<InetAddress, NetAddressError> =>
1334 isIpv4Address(address) ? inetAddressV4(address, port) : inetAddressV6(address, port)
1335
1336/**
1337 * Creates an internet address from a trusted IP address and port, throwing on failure.
1338 *
1339 * @stability unstable
1340 * @category unsafe
1341 * @since 4.0.0
1342 */
1343export const inetAddressUnsafe = (address: IpAddress, port: number): InetAddress =>
1344 Result.getOrThrow(inetAddress(address, port))
1345
1346/**
1347 * Creates a checked internet address from a numeric IP string and port.
1348 *
1349 * **Gotchas**
1350 *
1351 * The address must be a numeric IPv4 or IPv6 literal. This function does not
1352 * resolve hostnames or accept IPv6 brackets.
1353 *
1354 * @stability unstable
1355 * @category constructors
1356 * @since 4.0.0
1357 */
1358export const inetAddressFromIpString = (
1359 address: string,
1360 port: number
1361): Result.Result<InetAddress, NetAddressError> => {
1362 return Result.flatMap(ipFromString(address), (address) => inetAddress(address, port))
1363}
1364
1365/**
1366 * Creates an internet address from a trusted numeric IP string and port.
1367 *
1368 * **Gotchas**
1369 *
1370 * This function throws when either input is invalid. Use only when both values
1371 * are already known to satisfy the checked constructor's requirements.
1372 *
1373 * @stability unstable
1374 * @category unsafe
1375 * @since 4.0.0
1376 */
1377export const inetAddressFromIpStringUnsafe = (address: string, port: number): InetAddress =>
1378 Result.getOrThrow(inetAddressFromIpString(address, port))
1379
1380/**
1381 * Parses an unbracketed numeric host and port, resolving named IPv6 zones using
1382 * a supplied map of interface names to numeric scope IDs.
1383 *
1384 * **Details**
1385 *
1386 * IPv4, unscoped IPv6, and numeric IPv6 zones need no map. Named zones must have
1387 * a matching map entry. This function performs no DNS or operating-system lookup.
1388 *
1389 * @see {@link formatHost} for formatting the host of an internet address
1390 * @see {@link scopeIdsFromInterfaces} for building a scope map from interface entries
1391 * @stability unstable
1392 * @category decoding
1393 * @since 4.0.0
1394 */
1395export const inetAddressFromHostString = (
1396 host: string,
1397 port: number,
1398 scopeIds?: ReadonlyMap<string, number>
1399): Result.Result<InetAddress, NetAddressError> => {
1400 const separator = host.indexOf("%")
1401 if (separator !== -1) {
1402 const zone = host.slice(separator + 1)
1403 if (zone.length === 0 || zone.includes("%")) {
1404 return addressError(host, "invalid IPv6 scope identifier")
1405 }
1406 if (!/^\d+$/.test(zone)) {
1407 const scopeId = scopeIds?.get(zone)
1408 if (scopeId === undefined) {
1409 return addressError(host, `unknown IPv6 interface: ${zone}`)
1410 }
1411 host = `${host.slice(0, separator)}%${scopeId}`
1412 }
1413 }
1414 return inetAddressFromString(host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`)
1415}
1416
1417/**
1418 * Network interface address metadata used to resolve IPv6 scope IDs.
1419 *
1420 * @see {@link scopeIdsFromInterfaces}
1421 * @stability unstable
1422 * @category models
1423 * @since 4.0.0
1424 */
1425export interface NetworkInterfaceAddress {
1426 readonly family: string
1427 readonly scopeid?: number | undefined
1428}
1429
1430/**
1431 * Creates a map from interface names to IPv6 scope IDs using supplied interface
1432 * entries.
1433 *
1434 * **Details**
1435 *
1436 * Each entry pairs an interface name with its addresses. The first IPv6 address
1437 * with a positive scope ID supplies that interface's mapping. Accepts entries
1438 * such as `Object.entries(os.networkInterfaces())` without performing any
1439 * operating-system lookup itself. Later changes to the entries do not affect
1440 * the map.
1441 *
1442 * @see {@link inetAddressFromHostString} for resolving named IPv6 zones with the map
1443 * @stability unstable
1444 * @category converting
1445 * @since 4.0.0
1446 */
1447export const scopeIdsFromInterfaces = (
1448 interfaces: Iterable<readonly [name: string, addresses: ReadonlyArray<NetworkInterfaceAddress> | undefined]>
1449): Map<string, number> => {
1450 const scopeIds = new Map<string, number>()
1451 for (const [name, addresses] of interfaces) {
1452 const address = addresses?.find((address) => address.family === "IPv6" && (address.scopeid ?? 0) > 0)
1453 if (address?.scopeid !== undefined) scopeIds.set(name, address.scopeid)
1454 }
1455 return scopeIds
1456}
1457
1458/**
1459 * Parses `IPv4:port` or `[IPv6]:port` without DNS resolution.
1460 *
1461 * @stability unstable
1462 * @category decoding
1463 * @since 4.0.0
1464 */
1465export const inetAddressFromString = (input: string): Result.Result<InetAddress, NetAddressError> => {
1466 let host: string
1467 let portText: string
1468 let scopeId = 0
1469 const bracketed = input.startsWith("[")
1470 if (bracketed) {
1471 const end = input.indexOf("]")
1472 if (end < 0 || input[end + 1] !== ":" || input.indexOf("]", end + 1) !== -1) {
1473 return addressError(input, "expected [IPv6]:port")
1474 }
1475 host = input.slice(1, end)
1476 portText = input.slice(end + 2)
1477 const scopeSeparator = host.indexOf("%")
1478 if (scopeSeparator !== -1) {
1479 const scopeText = host.slice(scopeSeparator + 1)
1480 if (host.indexOf("%", scopeSeparator + 1) !== -1 || !/^\d+$/.test(scopeText)) {
1481 return addressError(input, "scope identifier must be decimal")
1482 }
1483 scopeId = Number(scopeText)
1484 if (!Number.isInteger(scopeId) || scopeId > 0xffffffff) {
1485 return addressError(input, "scope identifier must be an unsigned 32-bit integer")
1486 }
1487 host = host.slice(0, scopeSeparator)
1488 }
1489 } else {
1490 const separator = input.lastIndexOf(":")
1491 if (separator < 0) {
1492 return addressError(input, "expected host:port or [IPv6]:port")
1493 }
1494 if (input.indexOf(":") !== separator) {
1495 return addressError(input, "IPv6 addresses must be bracketed")
1496 }
1497 host = input.slice(0, separator)
1498 portText = input.slice(separator + 1)
1499 }
1500 if (!/^(0|[1-9]\d*)$/.test(portText)) {
1501 return addressError(input, "port must be an unpadded decimal integer")
1502 }
1503 return Result.flatMap(ipFromString(host), (address): Result.Result<InetAddress, NetAddressError> => {
1504 if (bracketed !== isIpv6Address(address)) {
1505 return addressError(input, "only IPv6 addresses use brackets")
1506 }
1507 return isIpv6Address(address)
1508 ? inetAddressV6(address, Number(portText), { scopeId })
1509 : inetAddressV4(address, Number(portText))
1510 })
1511}
1512
1513/**
1514 * Parses a trusted numeric internet address and port, throwing on failure.
1515 *
1516 * @stability unstable
1517 * @category unsafe
1518 * @since 4.0.0
1519 */
1520export const inetAddressFromStringUnsafe = (input: string): InetAddress =>
1521 Result.getOrThrow(inetAddressFromString(input))
1522
1523/**
1524 * Formats the numeric host of an internet address without brackets or a port,
1525 * preserving a nonzero IPv6 scope ID as a `%` suffix.
1526 *
1527 * **When to use**
1528 *
1529 * Use when the host and port are represented separately.
1530 *
1531 * @see {@link formatInet} for a complete socket address
1532 * @see {@link formatUrlHost} for a bracketed URL authority host
1533 * @stability unstable
1534 * @category encoding
1535 * @since 4.0.0
1536 */
1537export const formatHost = (self: InetAddress): string =>
1538 formatIp(self.address) + (isInetAddressV6(self) && self.scopeId !== 0 ? `%${self.scopeId}` : "")
1539
1540/**
1541 * Formats an internet address's host for a native socket API, without brackets
1542 * or a port.
1543 *
1544 * **Details**
1545 *
1546 * On `"win32"`, IPv6 zones remain numeric. On other platforms, the first
1547 * interface name matching the scope ID is used, falling back to the numeric ID
1548 * when no name matches. Unscoped IPv6 and IPv4 hosts are unchanged.
1549 *
1550 * Supply a scope map from {@link scopeIdsFromInterfaces} and optionally a platform
1551 * string. Omitting the platform uses non-Windows behavior. This function performs
1552 * no operating-system lookups.
1553 *
1554 * @stability unstable
1555 * @category encoding
1556 * @since 4.0.0
1557 */
1558export const formatNativeHost = (
1559 self: InetAddress,
1560 scopeIds: ReadonlyMap<string, number>,
1561 platform?: string
1562): string => {
1563 if (platform !== "win32" && isInetAddressV6(self) && self.scopeId !== 0) {
1564 for (const [name, scopeId] of scopeIds) {
1565 if (scopeId === self.scopeId) return `${formatIp(self.address)}%${name}`
1566 }
1567 }
1568 return formatHost(self)
1569}
1570
1571/**
1572 * Formats an IPv4 address or IPv6 interface index for a native multicast API.
1573 *
1574 * **Details**
1575 *
1576 * IPv4 addresses are formatted as numeric IPs. An IPv6 index of zero produces
1577 * `"::"`. Other indices produce `"::%index"` on `"win32"`, or `"::%name"` on
1578 * other platforms using the first matching interface name, falling back to the
1579 * numeric index when no name matches.
1580 *
1581 * Supply a scope map from {@link scopeIdsFromInterfaces} and optionally a platform
1582 * string. Omitting the platform uses non-Windows behavior. This function performs
1583 * no operating-system lookups.
1584 *
1585 * @stability unstable
1586 * @category encoding
1587 * @since 4.0.0
1588 */
1589export const formatMulticastInterface = (
1590 networkInterface: MulticastInterface,
1591 scopeIds: ReadonlyMap<string, number>,
1592 platform?: string
1593): string => {
1594 if (typeof networkInterface !== "number") return formatIp(networkInterface)
1595 if (networkInterface === 0) return "::"
1596 if (platform !== "win32") {
1597 for (const [name, scopeId] of scopeIds) {
1598 if (scopeId === networkInterface) return `::%${name}`
1599 }
1600 }
1601 return `::%${networkInterface}`
1602}
1603
1604/**
1605 * Formats a resolved internet address, bracketing IPv6 around its port.
1606 *
1607 * @stability unstable
1608 * @category encoding
1609 * @since 4.0.0
1610 */
1611export const formatInet = (self: InetAddress): string => {
1612 const host = formatHost(self)
1613 return isInetAddressV4(self) ? `${host}:${self.port}` : `[${host}]:${self.port}`
1614}
1615
1616/**
1617 * Formats an IP address for use as a URL authority host.
1618 *
1619 * @stability unstable
1620 * @category encoding
1621 * @since 4.0.0
1622 */
1623export const formatUrlHost = (self: IpAddress): string => isIpv4Address(self) ? formatIp(self) : `[${formatIp(self)}]`
1624
1625/**
1626 * Converts an IP address or internet socket address to a WHATWG `URL`.
1627 *
1628 * **Details**
1629 *
1630 * Defaults to the HTTP scheme and uses standard URL normalization, including
1631 * IPv6 brackets and omission of default ports. Bare IP addresses have no
1632 * explicit port; internet socket addresses retain their port, including zero.
1633 * Unspecified addresses are preserved.
1634 *
1635 * **Gotchas**
1636 *
1637 * Returns a `NetAddressError` for scoped IPv6 addresses or inputs that the URL
1638 * constructor rejects. Supply the scheme without a trailing colon, for example
1639 * `"https"`.
1640 *
1641 * @stability unstable
1642 * @category conversions
1643 * @since 4.0.0
1644 */
1645export const toUrl = (self: IpAddress | InetAddress, scheme: string = "http"): Result.Result<URL, NetAddressError> => {
1646 if (self._tag === "InetAddressV6" && self.scopeId !== 0) {
1647 return addressError(self, "scoped IPv6 addresses are not supported by WHATWG URLs")
1648 }
1649 return Result.try({
1650 try: () => new URL(`${scheme}://${isInetAddress(self) ? formatInet(self) : formatUrlHost(self)}`),
1651 catch: (cause) => new NetAddressError({ input: self, message: "failed to construct URL", cause })
1652 })
1653}
1654
1655/**
1656 * Formats an IP or socket address as a URL display string.
1657 *
1658 * **Details**
1659 *
1660 * Defaults to HTTP, brackets IPv6 addresses, and omits default ports and the
1661 * trailing slash. Custom schemes such as `"tcp"` retain their scheme and host.
1662 * Unix socket addresses use {@link formatUnixPath}, preserving the raw path
1663 * with a `unix://` prefix regardless of the supplied scheme.
1664 *
1665 * **Gotchas**
1666 *
1667 * Returns a `NetAddressError` when URL conversion fails, including for scoped
1668 * IPv6 addresses.
1669 *
1670 * @stability unstable
1671 * @category encoding
1672 * @since 4.0.0
1673 */
1674export const formatUrl = (
1675 self: IpAddress | SocketAddress,
1676 scheme: string = "http"
1677): Result.Result<string, NetAddressError> =>
1678 isUnixPathAddress(self)
1679 ? Result.succeed(formatUnixPath(self))
1680 : Result.map(toUrl(self, scheme), (url) => `${url.protocol}//${url.host}`)
1681
1682/**
1683 * Formats an IP or socket address as a URL display string,
1684 * throwing a `NetAddressError` when conversion fails.
1685 *
1686 * @see {@link formatUrl} for the checked version and formatting behavior.
1687 *
1688 * @stability unstable
1689 * @category unsafe
1690 * @since 4.0.0
1691 */
1692export const formatUrlUnsafe = (self: IpAddress | SocketAddress, scheme: string = "http"): string =>
1693 Result.getOrThrow(formatUrl(self, scheme))
1694
1695/**
1696 * Formats a hostname or numeric IP address for use as a URL authority host.
1697 *
1698 * @stability unstable
1699 * @category encoding
1700 * @since 4.0.0
1701 */
1702export const formatUrlHostString = (host: string): string =>
1703 host.includes(":") && !host.startsWith("[") ? `[${host}]` : host
1704
1705const UnixPathProto = {
1706 _tag: "UnixPathAddress",
1707 [TypeId]: TypeId,
1708 [Equal.symbol](this: UnixPathAddress, that: Equal.Equal): boolean {
1709 return isUnixPathAddress(that) && this.path === that.path
1710 },
1711 [Hash.symbol](this: UnixPathAddress): number {
1712 return Hash.combine(Hash.string("UnixPathAddress"), Hash.string(this.path))
1713 },
1714 toString(this: UnixPathAddress): string {
1715 return this.path
1716 },
1717 toJSON(this: UnixPathAddress): string {
1718 return this.toString()
1719 },
1720 [NodeInspectSymbol](this: UnixPathAddress): string {
1721 return this.toJSON()
1722 }
1723}
1724
1725/**
1726 * Creates a Unix-domain filesystem address without normalizing its opaque path.
1727 *
1728 * @stability unstable
1729 * @category constructors
1730 * @since 4.0.0
1731 */
1732export const unixPathAddress = (path: string): UnixPathAddress => {
1733 const self = Object.create(UnixPathProto)
1734 self.path = path
1735 return Object.freeze(self)
1736}
1737
1738/**
1739 * Formats a Unix-domain socket path as a readable `unix://path` display string.
1740 *
1741 * **Details**
1742 *
1743 * Preserves the raw path without URL encoding or normalization. The resulting
1744 * string is intended for display.
1745 *
1746 * @stability unstable
1747 * @category encoding
1748 * @since 4.0.0
1749 */
1750export const formatUnixPath = (self: UnixPathAddress): string => `unix://${self.path}`
1751
1752/**
1753 * Converts a `SocketAddress.Input` to a concrete socket address.
1754 *
1755 * **Details**
1756 *
1757 * Numeric IP strings are parsed without hostname resolution. Invalid IP
1758 * literals, ports, and input shapes return a `NetAddressError`.
1759 *
1760 * @stability unstable
1761 * @category constructors
1762 * @since 4.0.0
1763 */
1764export const socketAddressFromInput = (
1765 input: SocketAddress.Input
1766): Result.Result<SocketAddress, NetAddressError> => {
1767 if (isSocketAddress(input)) return Result.succeed(input)
1768 if (typeof input === "string") return inetAddressFromString(input)
1769 if (hasProperty(input, "path")) {
1770 return typeof input.path === "string"
1771 ? Result.succeed(unixPathAddress(input.path))
1772 : addressError(input, "path must be a string")
1773 }
1774 if (!hasProperty(input, "address") || !hasProperty(input, "port")) {
1775 return addressError(input, "expected an address and port or a Unix path")
1776 }
1777 const address = typeof input.address === "string"
1778 ? ipFromString(input.address)
1779 : isIpAddress(input.address)
1780 ? Result.succeed(input.address)
1781 : addressError(input, "address must be an IP address or numeric IP string")
1782 return Result.flatMap(address, (address) => inetAddress(address, input.port as number))
1783}
1784
1785/**
1786 * Converts a trusted `SocketAddress.Input` to a concrete socket address,
1787 * throwing on failure.
1788 *
1789 * @stability unstable
1790 * @category unsafe
1791 * @since 4.0.0
1792 */
1793export const socketAddressFromInputUnsafe = (input: SocketAddress.Input): SocketAddress =>
1794 Result.getOrThrow(socketAddressFromInput(input))
1795
1796/**
1797 * Formats a portable socket address for human-readable output.
1798 *
1799 * @stability unstable
1800 * @category encoding
1801 * @since 4.0.0
1802 */
1803export const formatSocketAddress = (self: SocketAddress): string =>
1804 self._tag === "UnixPathAddress" ? self.path : formatInet(self)