| 1 | /** |
| 2 | * Pure, canonical IPv4 and IPv6 network prefixes using CIDR notation. |
| 3 | * |
| 4 | * @stability unstable |
| 5 | * @since 4.0.0 |
| 6 | */ |
| 7 | import * as Equal from "../Equal.ts" |
| 8 | import { dual } from "../Function.ts" |
| 9 | import * as Hash from "../Hash.ts" |
| 10 | import { NodeInspectSymbol } from "../Inspectable.ts" |
| 11 | import { hasProperty } from "../Predicate.ts" |
| 12 | import * as Result from "../Result.ts" |
| 13 | import * as IpInterface from "./IpInterface.ts" |
| 14 | import * as NetAddress from "./NetAddress.ts" |
| 15 | |
| 16 | const 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 | */ |
| 28 | export 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 | */ |
| 44 | export 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 | */ |
| 53 | export 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 | */ |
| 62 | export 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 | */ |
| 71 | export 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 | */ |
| 80 | export const isIpNetwork = (u: unknown): u is IpNetwork => hasProperty(u, TypeId) |
| 81 | |
| 82 | const 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 | |
| 102 | const 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 | |
| 110 | const toBytes = (address: NetAddress.IpAddress): ReadonlyArray<number> => |
| 111 | NetAddress.isIpv4Address(address) ? NetAddress.ipv4ToOctets(address) : NetAddress.ipv6ToOctets(address) |
| 112 | |
| 113 | type IpAddressFamily<A extends NetAddress.IpAddress> = A extends NetAddress.Ipv4Address ? NetAddress.Ipv4Address |
| 114 | : A extends NetAddress.Ipv6Address ? NetAddress.Ipv6Address |
| 115 | : never |
| 116 | |
| 117 | const 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 | |
| 127 | const 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 | */ |
| 148 | export 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 | */ |
| 177 | export 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 | */ |
| 195 | export 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 | */ |
| 206 | export 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 | */ |
| 220 | export 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 | */ |
| 234 | export 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 | */ |
| 248 | export 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 | */ |
| 260 | export 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 | */ |
| 272 | export 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 | */ |
| 281 | export 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 | */ |
| 290 | export 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 | */ |
| 301 | export 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 | */ |
| 314 | export const addressCount = (self: IpNetwork): bigint => |
| 315 | BigInt(1) << BigInt(NetAddress.width(self.address) - self.prefixLength) |
| 316 | |
| 317 | const 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 | */ |
| 331 | export 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 | */ |
| 343 | export 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 | */ |
| 357 | export 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 | }) |