1/**
2 * Pure, canonical IPv4 and IPv6 network prefixes using CIDR notation.
3 *
4 * @stability unstable
5 * @since 4.0.0
6 */
7import * as Equal from "../Equal.ts"
8import { dual } from "../Function.ts"
9import * as Hash from "../Hash.ts"
10import { NodeInspectSymbol } from "../Inspectable.ts"
11import { hasProperty } from "../Predicate.ts"
12import * as Result from "../Result.ts"
13import * as IpInterface from "./IpInterface.ts"
14import * as NetAddress from "./NetAddress.ts"
15
16const TypeId = "~effect/net/IpNetwork" as const
17
18/**
19 * An immutable canonical IP network prefix. Constructors that retain the exact
20 * input address also retain its verified refinements. Operations that derive a
21 * different address return only its IPv4 or IPv6 family; revalidate the result
22 * before treating it as refined.
23 *
24 * @stability unstable
25 * @category models
26 * @since 4.0.0
27 */
28export interface IpNetwork<out A extends NetAddress.IpAddress = NetAddress.IpAddress> extends Equal.Equal, Hash.Hash {
29 readonly _tag: "IpNetwork"
30 readonly address: A
31 readonly prefixLength: number
32 readonly [TypeId]: typeof TypeId
33 toString(): string
34 toJSON(): string
35}
36
37/**
38 * An immutable canonical IPv4 network prefix.
39 *
40 * @stability unstable
41 * @category models
42 * @since 4.0.0
43 */
44export type Ipv4Network = IpNetwork<NetAddress.Ipv4Address>
45
46/**
47 * An immutable canonical IPv6 network prefix.
48 *
49 * @stability unstable
50 * @category models
51 * @since 4.0.0
52 */
53export type Ipv6Network = IpNetwork<NetAddress.Ipv6Address>
54
55/**
56 * Returns `true` when a value is an IPv4 network prefix.
57 *
58 * @stability unstable
59 * @category guards
60 * @since 4.0.0
61 */
62export const isIpv4Network = (u: unknown): u is Ipv4Network => isIpNetwork(u) && NetAddress.isIpv4Address(u.address)
63
64/**
65 * Returns `true` when a value is an IPv6 network prefix.
66 *
67 * @stability unstable
68 * @category guards
69 * @since 4.0.0
70 */
71export const isIpv6Network = (u: unknown): u is Ipv6Network => isIpNetwork(u) && NetAddress.isIpv6Address(u.address)
72
73/**
74 * Returns `true` when a value is an IPv4 or IPv6 network prefix.
75 *
76 * @stability unstable
77 * @category guards
78 * @since 4.0.0
79 */
80export const isIpNetwork = (u: unknown): u is IpNetwork => hasProperty(u, TypeId)
81
82const IpNetworkProto = {
83 _tag: "IpNetwork",
84 [TypeId]: TypeId,
85 [Equal.symbol](this: IpNetwork, that: Equal.Equal): boolean {
86 return isIpNetwork(that) && this.prefixLength === that.prefixLength && Equal.equals(this.address, that.address)
87 },
88 [Hash.symbol](this: IpNetwork): number {
89 return Hash.combine(Hash.hash(this.address), Hash.number(this.prefixLength))
90 },
91 toString(this: IpNetwork): string {
92 return format(this)
93 },
94 toJSON(this: IpNetwork): string {
95 return this.toString()
96 },
97 [NodeInspectSymbol](this: IpNetwork): string {
98 return this.toJSON()
99 }
100}
101
102const fromInterfaceValue = <A extends NetAddress.IpAddress>(value: IpInterface.IpInterface<A>): IpNetwork<A> => {
103 const self = Object.assign(Object.create(IpNetworkProto), {
104 address: value.address,
105 prefixLength: value.prefixLength
106 })
107 return Object.freeze(self)
108}
109
110const toBytes = (address: NetAddress.IpAddress): ReadonlyArray<number> =>
111 NetAddress.isIpv4Address(address) ? NetAddress.ipv4ToOctets(address) : NetAddress.ipv6ToOctets(address)
112
113type IpAddressFamily<A extends NetAddress.IpAddress> = A extends NetAddress.Ipv4Address ? NetAddress.Ipv4Address
114 : A extends NetAddress.Ipv6Address ? NetAddress.Ipv6Address
115 : never
116
117const fromBytes = <A extends NetAddress.IpAddress>(
118 address: A,
119 bytes: ReadonlyArray<number>
120): IpAddressFamily<A> => {
121 const array = new Uint8Array(bytes)
122 return (NetAddress.isIpv4Address(address)
123 ? NetAddress.ipv4FromBytesUnsafe(array)
124 : NetAddress.ipv6FromBytesUnsafe(array)) as IpAddressFamily<A>
125}
126
127const maskBytes = (bytes: ReadonlyArray<number>, prefixLength: number): Array<number> => {
128 const wholeBytes = Math.floor(prefixLength / 8)
129 const partialBits = prefixLength % 8
130 return bytes.map((byte, index) => {
131 if (index < wholeBytes) return byte
132 if (index === wholeBytes && partialBits !== 0) return byte & ((0xff << (8 - partialBits)) & 0xff)
133 return 0
134 })
135}
136
137/**
138 * Creates a network prefix when the address has no host bits set.
139 *
140 * **Gotchas**
141 *
142 * Use {@link fromAddress} when a host address should be truncated to its containing network.
143 *
144 * @stability unstable
145 * @category constructors
146 * @since 4.0.0
147 */
148export const make = <A extends NetAddress.IpAddress>(
149 address: A,
150 prefixLength: number
151): Result.Result<IpNetwork<A>, NetAddress.NetAddressError> => {
152 return Result.flatMap(IpInterface.make(address, prefixLength), (value) => {
153 const bytes = toBytes(address)
154 const masked = maskBytes(bytes, prefixLength)
155 if (bytes.some((byte, index) => byte !== masked[index])) {
156 return Result.fail(
157 new NetAddress.NetAddressError({ input: address, message: "address has non-zero host bits" })
158 )
159 }
160 return Result.succeed(fromInterfaceValue(value))
161 })
162}
163
164/**
165 * Creates the canonical network containing an address by clearing its host bits.
166 *
167 * **Gotchas**
168 *
169 * The returned network's address can differ from the input address.
170 * Its type therefore retains the IPv4 or IPv6 family but not refinements on
171 * the input address. Revalidate the derived address to recover a refinement.
172 *
173 * @stability unstable
174 * @category constructors
175 * @since 4.0.0
176 */
177export const fromAddress = <A extends NetAddress.IpAddress>(
178 address: A,
179 prefixLength: number
180): Result.Result<IpNetwork<IpAddressFamily<A>>, NetAddress.NetAddressError> => {
181 return Result.map(IpInterface.make(address, prefixLength), (value) => {
182 const masked = fromBytes(address, maskBytes(toBytes(address), prefixLength))
183 return fromInterfaceValue(IpInterface.makeUnsafe(masked, value.prefixLength))
184 })
185}
186
187/**
188 * Returns the canonical network containing an interface address. The derived
189 * address retains its IPv4 or IPv6 family but not refinements on the input.
190 *
191 * @stability unstable
192 * @category conversions
193 * @since 4.0.0
194 */
195export const fromInterface = <A extends NetAddress.IpAddress>(
196 self: IpInterface.IpInterface<A>
197): IpNetwork<IpAddressFamily<A>> => fromAddressUnsafe(self.address, self.prefixLength)
198
199/**
200 * Parses a strict IPv4 network prefix in CIDR notation.
201 *
202 * @stability unstable
203 * @category decoding
204 * @since 4.0.0
205 */
206export const ipv4FromString = (input: string): Result.Result<Ipv4Network, NetAddress.NetAddressError> => {
207 return Result.flatMap(
208 IpInterface.ipv4FromString(input, { prefix: "required" }),
209 (value) => make(value.address, value.prefixLength)
210 )
211}
212
213/**
214 * Parses a strict IPv6 network prefix in CIDR notation.
215 *
216 * @stability unstable
217 * @category decoding
218 * @since 4.0.0
219 */
220export const ipv6FromString = (input: string): Result.Result<Ipv6Network, NetAddress.NetAddressError> => {
221 return Result.flatMap(
222 IpInterface.ipv6FromString(input, { prefix: "required" }),
223 (value) => make(value.address, value.prefixLength)
224 )
225}
226
227/**
228 * Parses a strict IPv4 or IPv6 network prefix in CIDR notation.
229 *
230 * @stability unstable
231 * @category decoding
232 * @since 4.0.0
233 */
234export const fromString = (input: string): Result.Result<IpNetwork, NetAddress.NetAddressError> => {
235 return Result.flatMap(
236 IpInterface.fromString(input, { prefix: "required" }),
237 (value) => make(value.address, value.prefixLength)
238 )
239}
240
241/**
242 * Creates a trusted network prefix, throwing when its address or prefix is invalid.
243 *
244 * @stability unstable
245 * @category unsafe
246 * @since 4.0.0
247 */
248export const makeUnsafe = <A extends NetAddress.IpAddress>(address: A, prefixLength: number): IpNetwork<A> =>
249 Result.getOrThrow(make(address, prefixLength))
250
251/**
252 * Creates the network containing a trusted address, throwing when its prefix is invalid.
253 * The derived address retains its IPv4 or IPv6 family but not refinements on
254 * the input address.
255 *
256 * @stability unstable
257 * @category unsafe
258 * @since 4.0.0
259 */
260export const fromAddressUnsafe = <A extends NetAddress.IpAddress>(
261 address: A,
262 prefixLength: number
263): IpNetwork<IpAddressFamily<A>> => Result.getOrThrow(fromAddress(address, prefixLength))
264
265/**
266 * Parses a trusted network prefix in CIDR notation, throwing on failure.
267 *
268 * @stability unstable
269 * @category unsafe
270 * @since 4.0.0
271 */
272export const fromStringUnsafe = (input: string): IpNetwork => Result.getOrThrow(fromString(input))
273
274/**
275 * Formats a network prefix using canonical address text and its decimal prefix length.
276 *
277 * @stability unstable
278 * @category encoding
279 * @since 4.0.0
280 */
281export const format = (self: IpNetwork): string => `${NetAddress.formatIp(self.address)}/${self.prefixLength}`
282
283/**
284 * Returns the lowest address in a network prefix.
285 *
286 * @stability unstable
287 * @category getters
288 * @since 4.0.0
289 */
290export const firstAddress = <A extends NetAddress.IpAddress>(self: IpNetwork<A>): A => self.address
291
292/**
293 * Returns the numerically greatest address in a network prefix. The derived
294 * address retains its IPv4 or IPv6 family but not refinements on the network
295 * address.
296 *
297 * @stability unstable
298 * @category getters
299 * @since 4.0.0
300 */
301export const lastAddress = <A extends NetAddress.IpAddress>(self: IpNetwork<A>): IpAddressFamily<A> => {
302 const bytes = toBytes(self.address)
303 const networkMask = maskBytes(bytes.map(() => 0xff), self.prefixLength)
304 return fromBytes(self.address, bytes.map((byte, index) => byte | (networkMask[index] ^ 0xff)))
305}
306
307/**
308 * Returns the exact number of addresses in a network prefix.
309 *
310 * @stability unstable
311 * @category getters
312 * @since 4.0.0
313 */
314export const addressCount = (self: IpNetwork): bigint =>
315 BigInt(1) << BigInt(NetAddress.width(self.address) - self.prefixLength)
316
317const containsBytes = (self: IpNetwork, address: NetAddress.IpAddress): boolean => {
318 if (NetAddress.isIpv4Address(self.address) !== NetAddress.isIpv4Address(address)) return false
319 const expected = toBytes(self.address)
320 const actual = maskBytes(toBytes(address), self.prefixLength)
321 return expected.every((byte, index) => byte === actual[index])
322}
323
324/**
325 * Returns `true` when a network prefix contains an IP address.
326 *
327 * @stability unstable
328 * @category predicates
329 * @since 4.0.0
330 */
331export const contains: {
332 (address: NetAddress.IpAddress): (self: IpNetwork) => boolean
333 (self: IpNetwork, address: NetAddress.IpAddress): boolean
334} = dual(2, containsBytes)
335
336/**
337 * Returns `true` when a network prefix contains every address in another network prefix.
338 *
339 * @stability unstable
340 * @category predicates
341 * @since 4.0.0
342 */
343export const containsNetwork: {
344 (other: IpNetwork): (self: IpNetwork) => boolean
345 (self: IpNetwork, other: IpNetwork): boolean
346} = dual(2, (self: IpNetwork, other: IpNetwork): boolean => {
347 return self.prefixLength <= other.prefixLength && containsBytes(self, other.address)
348})
349
350/**
351 * Returns `true` when two network prefixes contain at least one common address.
352 *
353 * @stability unstable
354 * @category predicates
355 * @since 4.0.0
356 */
357export const overlaps: {
358 (other: IpNetwork): (self: IpNetwork) => boolean
359 (self: IpNetwork, other: IpNetwork): boolean
360} = dual(2, (self: IpNetwork, other: IpNetwork): boolean => {
361 return self.prefixLength <= other.prefixLength
362 ? containsBytes(self, other.address)
363 : containsBytes(other, self.address)
364})