1/**
2 * IPv4 and IPv6 interface addresses that preserve host bits alongside a prefix length.
3 *
4 * @stability unstable
5 * @since 4.0.0
6 */
7import * as Equal from "../Equal.ts"
8import * as Hash from "../Hash.ts"
9import { NodeInspectSymbol } from "../Inspectable.ts"
10import { hasProperty } from "../Predicate.ts"
11import * as Result from "../Result.ts"
12import * as NetAddress from "./NetAddress.ts"
13
14const TypeId = "~effect/net/IpInterface" as const
15
16/**
17 * An IP host address and prefix length. Host bits and any verified address
18 * refinements are preserved because the address is retained unchanged. Code
19 * that computes different address bits must widen to the IPv4 or IPv6 family,
20 * or validate the resulting address before treating it as refined.
21 *
22 * @stability unstable
23 * @category models
24 * @since 4.0.0
25 */
26export interface IpInterface<out A extends NetAddress.IpAddress = NetAddress.IpAddress> extends Equal.Equal, Hash.Hash {
27 readonly _tag: "IpInterface"
28 readonly address: A
29 readonly prefixLength: number
30 readonly [TypeId]: typeof TypeId
31 toString(): string
32 toJSON(): string
33}
34
35/**
36 * An IPv4 host address and prefix length.
37 *
38 * @stability unstable
39 * @category models
40 * @since 4.0.0
41 */
42export type Ipv4Interface = IpInterface<NetAddress.Ipv4Address>
43
44/**
45 * An IPv6 host address and prefix length.
46 *
47 * @stability unstable
48 * @category models
49 * @since 4.0.0
50 */
51export type Ipv6Interface = IpInterface<NetAddress.Ipv6Address>
52
53/**
54 * Companion types for parsing IP interface addresses.
55 *
56 * @stability unstable
57 * @since 4.0.0
58 */
59export declare namespace IpInterface {
60 /**
61 * Controls whether the input must contain an explicit prefix. Prefixes are
62 * optional by default and use the address width when omitted.
63 *
64 * @stability unstable
65 * @category models
66 * @since 4.0.0
67 */
68 export interface ParseOptions {
69 readonly prefix?: "required" | "optional"
70 }
71}
72
73/**
74 * Returns `true` when a value is an IPv4 interface address.
75 *
76 * @stability unstable
77 * @category guards
78 * @since 4.0.0
79 */
80export const isIpv4Interface = (u: unknown): u is Ipv4Interface =>
81 isIpInterface(u) && NetAddress.isIpv4Address(u.address)
82
83/**
84 * Returns `true` when a value is an IPv6 interface address.
85 *
86 * @stability unstable
87 * @category guards
88 * @since 4.0.0
89 */
90export const isIpv6Interface = (u: unknown): u is Ipv6Interface =>
91 isIpInterface(u) && NetAddress.isIpv6Address(u.address)
92
93/**
94 * Returns `true` when a value is an IP interface address.
95 *
96 * @stability unstable
97 * @category guards
98 * @since 4.0.0
99 */
100export const isIpInterface = (u: unknown): u is IpInterface => hasProperty(u, TypeId)
101
102const IpInterfaceProto = {
103 _tag: "IpInterface",
104 [TypeId]: TypeId,
105 [Equal.symbol](this: IpInterface, that: Equal.Equal): boolean {
106 return isIpInterface(that) &&
107 this.prefixLength === that.prefixLength &&
108 Equal.equals(this.address, that.address)
109 },
110 [Hash.symbol](this: IpInterface): number {
111 return Hash.combine(Hash.hash(this.address), Hash.number(this.prefixLength))
112 },
113 toString(this: IpInterface): string {
114 return format(this)
115 },
116 toJSON(this: IpInterface): string {
117 return this.toString()
118 },
119 [NodeInspectSymbol](this: IpInterface): string {
120 return this.toJSON()
121 }
122}
123
124const interfaceError = (input: unknown, message: string): Result.Result<never, NetAddress.NetAddressError> =>
125 Result.fail(new NetAddress.NetAddressError({ input, message }))
126
127/**
128 * Creates an interface address while preserving all address bits.
129 *
130 * @stability unstable
131 * @category constructors
132 * @since 4.0.0
133 */
134export const make = <A extends NetAddress.IpAddress>(
135 address: A,
136 prefixLength: number
137): Result.Result<IpInterface<A>, NetAddress.NetAddressError> => {
138 const max = NetAddress.width(address)
139 if (!Number.isInteger(prefixLength) || prefixLength < 0 || prefixLength > max) {
140 return interfaceError(address, `prefix length must be an integer from 0 through ${max}`)
141 }
142 const self = Object.assign(Object.create(IpInterfaceProto), { address, prefixLength })
143 return Result.succeed(Object.freeze(self))
144}
145
146const parseAddressWithPrefix = (
147 input: string,
148 options?: IpInterface.ParseOptions
149): Result.Result<
150 { readonly address: string; readonly prefixLength: number | undefined },
151 NetAddress.NetAddressError
152> => {
153 const slash = input.indexOf("/")
154 if (slash === -1 && options?.prefix !== "required") {
155 return Result.succeed({ address: input, prefixLength: undefined })
156 }
157 if (slash <= 0 || slash !== input.lastIndexOf("/") || slash === input.length - 1) {
158 return interfaceError(input, "expected an address and prefix length separated by one slash")
159 }
160 const prefix = input.slice(slash + 1)
161 if (!/^(0|[1-9][0-9]*)$/.test(prefix)) {
162 return interfaceError(input, "prefix length must be an unpadded ASCII decimal integer")
163 }
164 return Result.succeed({ address: input.slice(0, slash), prefixLength: Number(prefix) })
165}
166
167/**
168 * Parses an IPv4 interface address while preserving host bits.
169 *
170 * **Details**
171 *
172 * A missing prefix defaults to 32. Use `prefix: "required"` to require one.
173 *
174 * @stability unstable
175 * @category decoding
176 * @since 4.0.0
177 */
178export const ipv4FromString = (
179 input: string,
180 options?: IpInterface.ParseOptions
181): Result.Result<Ipv4Interface, NetAddress.NetAddressError> =>
182 Result.flatMap(
183 parseAddressWithPrefix(input, options),
184 (parts) =>
185 Result.flatMap(NetAddress.ipv4FromString(parts.address), (address) =>
186 make(address, parts.prefixLength ?? NetAddress.width(address)))
187 )
188
189/**
190 * Parses an IPv6 interface address while preserving host bits.
191 *
192 * **Details**
193 *
194 * A missing prefix defaults to 128. Use `prefix: "required"` to require one.
195 *
196 * @stability unstable
197 * @category decoding
198 * @since 4.0.0
199 */
200export const ipv6FromString = (
201 input: string,
202 options?: IpInterface.ParseOptions
203): Result.Result<Ipv6Interface, NetAddress.NetAddressError> =>
204 Result.flatMap(
205 parseAddressWithPrefix(input, options),
206 (parts) =>
207 Result.flatMap(NetAddress.ipv6FromString(parts.address), (address) =>
208 make(address, parts.prefixLength ?? NetAddress.width(address)))
209 )
210
211/**
212 * Parses an IP interface address while preserving host bits.
213 *
214 * **Details**
215 *
216 * A missing prefix defaults to the address width. Use `prefix: "required"` to
217 * require one.
218 *
219 * @stability unstable
220 * @category decoding
221 * @since 4.0.0
222 */
223export const fromString = (
224 input: string,
225 options?: IpInterface.ParseOptions
226): Result.Result<IpInterface, NetAddress.NetAddressError> =>
227 Result.flatMap(
228 parseAddressWithPrefix(input, options),
229 (parts) =>
230 Result.flatMap(NetAddress.ipFromString(parts.address), (address) =>
231 make(address, parts.prefixLength ?? NetAddress.width(address)))
232 )
233
234/**
235 * Creates a trusted interface address, throwing when its prefix length is invalid.
236 *
237 * @stability unstable
238 * @category unsafe
239 * @since 4.0.0
240 */
241export const makeUnsafe = <A extends NetAddress.IpAddress>(address: A, prefixLength: number): IpInterface<A> =>
242 Result.getOrThrow(make(address, prefixLength))
243
244/**
245 * Parses a trusted interface address, throwing on failure.
246 *
247 * @stability unstable
248 * @category unsafe
249 * @since 4.0.0
250 */
251export const fromStringUnsafe = (input: string, options?: IpInterface.ParseOptions): IpInterface =>
252 Result.getOrThrow(fromString(input, options))
253
254/**
255 * Formats an interface address using canonical address text and its decimal prefix length.
256 *
257 * @stability unstable
258 * @category encoding
259 * @since 4.0.0
260 */
261export const format = (self: IpInterface): string => `${NetAddress.formatIp(self.address)}/${self.prefixLength}`