UNPKG

the-moby-effect

Version:
288 lines (260 loc) 8.24 kB
/** * IPv6 address schema. * * @since 1.0.0 */ import * as Brand from "effect/Brand"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as IPv4 from "./ipv4.js"; /** * @since 1.0.0 * @category Regular expressions * @see https://github.com/nodejs/node/blob/e08a654fae0ecc91678819e0b62a2e014bad3339/lib/internal/net.js#L21-L31 */ export const IPv6Segment = "(?:[0-9a-fA-F]{1,4})"; /** * @since 1.0.0 * @category Regular expressions */ export const IPv6Regex = new RegExp( "^(?:" + `(?:${IPv6Segment}:){7}(?:${IPv6Segment}|:)|` + `(?:${IPv6Segment}:){6}(?:${IPv4.IPv4String}|:${IPv6Segment}|:)|` + `(?:${IPv6Segment}:){5}(?::${IPv4.IPv4String}|(?::${IPv6Segment}){1,2}|:)|` + `(?:${IPv6Segment}:){4}(?:(?::${IPv6Segment}){0,1}:${IPv4.IPv4String}|(?::${IPv6Segment}){1,3}|:)|` + `(?:${IPv6Segment}:){3}(?:(?::${IPv6Segment}){0,2}:${IPv4.IPv4String}|(?::${IPv6Segment}){1,4}|:)|` + `(?:${IPv6Segment}:){2}(?:(?::${IPv6Segment}){0,3}:${IPv4.IPv4String}|(?::${IPv6Segment}){1,5}|:)|` + `(?:${IPv6Segment}:){1}(?:(?::${IPv6Segment}){0,4}:${IPv4.IPv4String}|(?::${IPv6Segment}){1,6}|:)|` + `(?::(?:(?::${IPv6Segment}){0,5}:${IPv4.IPv4String}|(?::${IPv6Segment}){1,7}|:))` + ")(?:%[0-9a-zA-Z-.:]{1,})?$" ); /** * @since 1.0.0 * @category Api interface */ export interface $IPv6Family extends Schema.Literal<["ipv6"]> {} /** * @since 1.0.0 * @category Decoded types */ export type IPv6Family = Schema.Schema.Type<$IPv6Family>; /** * @since 1.0.0 * @category Schemas */ export const IPv6Family: $IPv6Family = Schema.Literal("ipv6").annotations({ identifier: "IPv6Family", description: "An ipv6 family", }); /** * @since 1.0.0 * @category Api interface */ export interface $IPv6String extends Schema.filter<Schema.Schema<string, string, never>> {} /** * An IPv6 address in string format. * * @since 1.0.0 * @category Schemas */ export const IPv6String: $IPv6String = Schema.String.pipe(Schema.pattern(IPv6Regex)); /** * @since 1.0.0 * @category Branded types */ export type IPv6Brand = string & Brand.Brand<"IPv6">; /** * @since 1.0.0 * @category Branded constructors */ export const IPv6Brand = Brand.nominal<IPv6Brand>(); /** * @since 1.0.0 * @category Api interface */ export interface $IPv6 extends Schema.transform< Schema.filter<Schema.Schema<string, string, never>>, Schema.Struct<{ family: $IPv6Family; ip: Schema.BrandSchema<IPv6Brand, Brand.Brand.Unbranded<IPv6Brand>, never>; }> > {} /** * @since 1.0.0 * @category Decoded types */ export type IPv6 = Schema.Schema.Type<$IPv6>; /** * @since 1.0.0 * @category Encoded types */ export type IPv6Encoded = Schema.Schema.Encoded<$IPv6>; /** * An IPv6 address. * * @since 1.0.0 * @category Schemas * @example * import * as Schema from "effect/Schema"; * import { IPv6 } from "the-moby-effect/schemas/index.js"; * * const decodeIPv6 = Schema.decodeSync(IPv6); * assert.deepEqual(decodeIPv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), { * family: "ipv6", * ip: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", * }); * * assert.throws(() => * decodeIPv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334:") * ); * assert.throws(() => decodeIPv6("2001::85a3::0000::0370:7334")); * assert.doesNotThrow(() => * decodeIPv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334") * ); */ export const IPv6: $IPv6 = Schema.transform( IPv6String, Schema.Struct({ family: IPv6Family, ip: Schema.String.pipe(Schema.fromBrand(IPv6Brand)), }), { encode: ({ ip }) => ip, decode: (ip) => ({ ip, family: "ipv6" }) as const, } ).annotations({ identifier: "IPv6", description: "An ipv6 address", }); /** * @since 1.0.0 * @category Branded types */ export type IPv6BigintBrand = bigint & Brand.Brand<"IPv6Bigint">; /** * @since 1.0.0 * @category Branded constructors */ export const IPv6BigintBrand = Brand.nominal<IPv6BigintBrand>(); /** * @since 1.0.0 * @category Api interface */ export interface $IPv6Bigint extends Schema.transformOrFail< $IPv6, Schema.Struct<{ family: $IPv6Family; value: Schema.BrandSchema<IPv6BigintBrand, Brand.Brand.Unbranded<IPv6BigintBrand>, never>; }>, never > {} /** * @since 1.0.0 * @category Decoded types */ export type IPv6Bigint = Schema.Schema.Type<$IPv6Bigint>; /** * @since 1.0.0 * @category Encoded types */ export type IPv6BigintEncoded = Schema.Schema.Encoded<$IPv6Bigint>; /** * An IPv6 as a bigint. * * @since 1.0.0 * @category Schemas * @example * import * as Schema from "effect/Schema"; * import { * IPv6Bigint, * IPv6BigintBrand, * } from "the-moby-effect/schemas/IPv6.js"; * * const y: IPv6BigintBrand = IPv6BigintBrand(748392749382n); * assert.strictEqual(y, 748392749382n); * * const decodeIPv6Bigint = Schema.decodeSync(IPv6Bigint); * const encodeIPv6Bigint = Schema.encodeSync(IPv6Bigint); * * assert.deepEqual( * decodeIPv6Bigint("4cbd:ff70:e62b:a048:686c:4e7e:a68a:c377"), * { value: 102007852745154114519525620108359287671n, family: "ipv6" } * ); * assert.deepEqual( * decodeIPv6Bigint("d8c6:3feb:46e6:b80c:5a07:6227:ac19:caf6"), * { value: 288142618299897818094313964584331496182n, family: "ipv6" } * ); * * assert.deepEqual( * encodeIPv6Bigint({ * value: IPv6BigintBrand(102007852745154114519525620108359287671n), * family: "ipv6", * }), * "4cbd:ff70:e62b:a048:686c:4e7e:a68a:c377" * ); * assert.deepEqual( * encodeIPv6Bigint({ * value: IPv6BigintBrand(288142618299897818094313964584331496182n), * family: "ipv6", * }), * "d8c6:3feb:46e6:b80c:5a07:6227:ac19:caf6" * ); */ export const IPv6Bigint: $IPv6Bigint = Schema.transformOrFail( IPv6, Schema.Struct({ family: IPv6Family, value: Schema.BigIntFromSelf.pipe(Schema.fromBrand(IPv6BigintBrand)), }), { encode: ({ value }) => { const hex = value.toString(16).padStart(32, "0"); const groups = []; for (let i = 0; i < 8; i++) { groups.push(hex.slice(i * 4, (i + 1) * 4)); } return Schema.decode(IPv6)(groups.join(":")).pipe(Effect.mapError(({ issue }) => issue)); }, decode: ({ ip }) => { function paddedHex(octet: string): string { return parseInt(octet, 16).toString(16).padStart(4, "0"); } let groups: Array<string> = []; const halves = ip.split("::"); if (halves.length === 2) { let first = halves[0]!.split(":"); let last = halves[1]!.split(":"); if (first.length === 1 && first[0] === "") { first = []; } if (last.length === 1 && last[0] === "") { last = []; } const remaining = 8 - (first.length + last.length); if (!remaining) { throw new Error("Error parsing groups"); } groups = groups.concat(first); for (let i = 0; i < remaining; i++) { groups.push("0"); } groups = groups.concat(last); } else if (halves.length === 1) { groups = ip.split(":"); } else { throw new Error("Too many :: groups found"); } groups = groups.map((group: string) => parseInt(group, 16).toString(16)); if (groups.length !== 8) { throw new Error("Invalid number of groups"); } return Effect.succeed({ value: BigInt(`0x${groups.map(paddedHex).join("")}`), family: "ipv6" } as const); }, } ).annotations({ identifier: "IPv6Bigint", description: "An ipv6 address as a bigint", });