From f82f817252c1f1342d81a2a5ae9adf426cb32cec Mon Sep 17 00:00:00 2001 From: Ryo Watanabe Date: Tue, 10 Dec 2024 10:38:09 +0900 Subject: [PATCH] feat: z.string.cidr() - support CIDR notation (#3820) * feat: support cidr * docs * feat: z.string().cidr() * fix * Simplify --------- Co-authored-by: Colin McDonnell --- README.md | 25 ++++++++++++- deno/lib/README.md | 25 ++++++++++++- deno/lib/ZodError.ts | 1 + deno/lib/__tests__/string.test.ts | 60 +++++++++++++++++++++++++++++++ deno/lib/types.ts | 33 +++++++++++++++++ src/ZodError.ts | 1 + src/__tests__/string.test.ts | 60 +++++++++++++++++++++++++++++++ src/types.ts | 33 +++++++++++++++++ 8 files changed, 236 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3cf25ab2d..c60c133fd 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ - [Dates](#dates) - [Times](#times) - [IP addresses](#ip-addresses) + - [IP ranges](#ip-ranges-cidr) - [Numbers](#numbers) - [BigInts](#bigints) - [NaNs](#nans) @@ -777,6 +778,7 @@ z.string().startsWith(string); z.string().endsWith(string); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().ip(); // defaults to allow both IPv4 and IPv6 +z.string().cidr(); // defaults to allow both IPv4 and IPv6 // transforms z.string().trim(); // trim whitespace @@ -818,6 +820,7 @@ z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); z.string().date({ message: "Invalid date string!" }); z.string().time({ message: "Invalid time string!" }); z.string().ip({ message: "Invalid IP address" }); +z.string().cidr({ message: "Invalid CIDR" }); ``` ### Datetimes @@ -900,7 +903,7 @@ time.parse("00:00:00"); // fail ### IP addresses -The `z.string().ip()` method by default validate IPv4 and IPv6. +By default `.ip()` allows both IPv4 and IPv6. ```ts const ip = z.string().ip(); @@ -923,6 +926,26 @@ const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail ``` +### IP ranges (CIDR) + +Validate IP address ranges specified with [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). By default, `.cidr()` allows both IPv4 and IPv6. + +```ts +const cidr = z.string().cidr(); +cidr.parse("192.168.0.0/24"); // pass +cidr.parse("2001:db8::/32"); // pass +``` + +You can specify a version with the `version` parameter. + +```ts +const ipv4Cidr = z.string().cidr({ version: "v4" }); +ipv4Cidr.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail + +const ipv6Cidr = z.string().cidr({ version: "v6" }); +ipv6Cidr.parse("192.168.1.1"); // fail +``` + ## Numbers You can customize certain error messages when creating a number schema. diff --git a/deno/lib/README.md b/deno/lib/README.md index a3c888695..2f5af57c2 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -76,6 +76,7 @@ - [Dates](#dates) - [Times](#times) - [IP addresses](#ip-addresses) + - [IP ranges](#ip-ranges-cidr) - [Numbers](#numbers) - [BigInts](#bigints) - [NaNs](#nans) @@ -787,6 +788,7 @@ z.string().startsWith(string); z.string().endsWith(string); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().ip(); // defaults to allow both IPv4 and IPv6 +z.string().cidr(); // defaults to allow both IPv4 and IPv6 // transforms z.string().trim(); // trim whitespace @@ -828,6 +830,7 @@ z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); z.string().date({ message: "Invalid date string!" }); z.string().time({ message: "Invalid time string!" }); z.string().ip({ message: "Invalid IP address" }); +z.string().cidr({ message: "Invalid CIDR" }); ``` ### Datetimes @@ -910,7 +913,7 @@ time.parse("00:00:00"); // fail ### IP addresses -The `z.string().ip()` method by default validate IPv4 and IPv6. +By default `.ip()` allows both IPv4 and IPv6. ```ts const ip = z.string().ip(); @@ -933,6 +936,26 @@ const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail ``` +### IP ranges (CIDR) + +Validate IP address ranges specified with [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). By default, `.cidr()` allows both IPv4 and IPv6. + +```ts +const cidr = z.string().cidr(); +cidr.parse("192.168.0.0/24"); // pass +cidr.parse("2001:db8::/32"); // pass +``` + +You can specify a version with the `version` parameter. + +```ts +const ipv4Cidr = z.string().cidr({ version: "v4" }); +ipv4Cidr.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail + +const ipv6Cidr = z.string().cidr({ version: "v6" }); +ipv6Cidr.parse("192.168.1.1"); // fail +``` + ## Numbers You can customize certain error messages when creating a number schema. diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index e757cd8ba..21ad657a3 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -103,6 +103,7 @@ export type StringValidation = | "time" | "duration" | "ip" + | "cidr" | "base64" | { includes: string; position?: number } | { startsWith: string } diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 64438717a..3c059f0b0 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -373,6 +373,7 @@ test("checks getters", () => { expect(z.string().email().isUUID).toEqual(false); expect(z.string().email().isNANOID).toEqual(false); expect(z.string().email().isIP).toEqual(false); + expect(z.string().email().isCIDR).toEqual(false); expect(z.string().email().isULID).toEqual(false); expect(z.string().url().isEmail).toEqual(false); @@ -382,6 +383,7 @@ test("checks getters", () => { expect(z.string().url().isUUID).toEqual(false); expect(z.string().url().isNANOID).toEqual(false); expect(z.string().url().isIP).toEqual(false); + expect(z.string().url().isCIDR).toEqual(false); expect(z.string().url().isULID).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); @@ -391,6 +393,7 @@ test("checks getters", () => { expect(z.string().cuid().isUUID).toEqual(false); expect(z.string().cuid().isNANOID).toEqual(false); expect(z.string().cuid().isIP).toEqual(false); + expect(z.string().cuid().isCIDR).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); @@ -400,6 +403,7 @@ test("checks getters", () => { expect(z.string().cuid2().isUUID).toEqual(false); expect(z.string().cuid2().isNANOID).toEqual(false); expect(z.string().cuid2().isIP).toEqual(false); + expect(z.string().cuid2().isCIDR).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); @@ -409,6 +413,7 @@ test("checks getters", () => { expect(z.string().uuid().isUUID).toEqual(true); expect(z.string().uuid().isNANOID).toEqual(false); expect(z.string().uuid().isIP).toEqual(false); + expect(z.string().uuid().isCIDR).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); expect(z.string().nanoid().isEmail).toEqual(false); @@ -418,6 +423,7 @@ test("checks getters", () => { expect(z.string().nanoid().isUUID).toEqual(false); expect(z.string().nanoid().isNANOID).toEqual(true); expect(z.string().nanoid().isIP).toEqual(false); + expect(z.string().nanoid().isCIDR).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); expect(z.string().ip().isEmail).toEqual(false); @@ -427,8 +433,19 @@ test("checks getters", () => { expect(z.string().ip().isUUID).toEqual(false); expect(z.string().ip().isNANOID).toEqual(false); expect(z.string().ip().isIP).toEqual(true); + expect(z.string().ip().isCIDR).toEqual(false); expect(z.string().ip().isULID).toEqual(false); + expect(z.string().cidr().isEmail).toEqual(false); + expect(z.string().cidr().isURL).toEqual(false); + expect(z.string().cidr().isCUID).toEqual(false); + expect(z.string().cidr().isCUID2).toEqual(false); + expect(z.string().cidr().isUUID).toEqual(false); + expect(z.string().cidr().isNANOID).toEqual(false); + expect(z.string().cidr().isIP).toEqual(false); + expect(z.string().cidr().isCIDR).toEqual(true); + expect(z.string().cidr().isULID).toEqual(false); + expect(z.string().ulid().isEmail).toEqual(false); expect(z.string().ulid().isURL).toEqual(false); expect(z.string().ulid().isCUID).toEqual(false); @@ -436,6 +453,7 @@ test("checks getters", () => { expect(z.string().ulid().isUUID).toEqual(false); expect(z.string().ulid().isNANOID).toEqual(false); expect(z.string().ulid().isIP).toEqual(false); + expect(z.string().ulid().isCIDR).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); }); @@ -769,3 +787,45 @@ test("IP validation", () => { invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); }); + +test("CIDR validation", () => { + const ipv4Cidr = z.string().cidr({ version: "v4" }); + expect(() => ipv4Cidr.parse("2001:0db8:85a3::8a2e:0370:7334/64")).toThrow(); + + const ipv6Cidr = z.string().cidr({ version: "v6" }); + expect(() => ipv6Cidr.parse("192.168.0.1/24")).toThrow(); + + const validCidrs = [ + "192.168.0.0/24", + "10.0.0.0/8", + "203.0.113.0/24", + "192.0.2.0/24", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.1.0/24", + "fc00::/7", + "fd00::/8", + "2001:db8::/32", + "2607:f0d0:1002:51::4/64", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128", + "2001:0db8:1234:0000::/64", + ]; + + const invalidCidrs = [ + "192.168.1.1/33", + "10.0.0.1/-1", + "192.168.1.1/24/24", + "192.168.1.0/abc", + "2001:db8::1/129", + "2001:db8::1/-1", + "2001:db8::1/64/64", + "2001:db8::1/abc", + ]; + + // no parameters check IPv4 or IPv6 + const cidrSchema = z.string().cidr(); + expect(validCidrs.every((ip) => cidrSchema.safeParse(ip).success)).toBe(true); + expect( + invalidCidrs.every((ip) => cidrSchema.safeParse(ip).success === false) + ).toBe(true); +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 9cee39a35..42d2606bb 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -565,6 +565,7 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } + | { kind: "cidr"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string }; export interface ZodStringDef extends ZodTypeDef { @@ -608,11 +609,15 @@ let emojiRegex: RegExp; // faster, simpler, safer const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = + /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; // const ipv6Regex = // /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = + /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; // https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript const base64Regex = @@ -671,6 +676,17 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +function isValidCidr(ip: string, version?: IpVersion) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + + return false; +} + export class ZodString extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { @@ -933,6 +949,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "base64") { if (!base64Regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1006,6 +1032,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } + cidr(options?: string | { version?: IpVersion; message?: string }) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime( options?: | string @@ -1199,6 +1229,9 @@ export class ZodString extends ZodType { get isIP() { return !!this._def.checks.find((ch) => ch.kind === "ip"); } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } get isBase64() { return !!this._def.checks.find((ch) => ch.kind === "base64"); } diff --git a/src/ZodError.ts b/src/ZodError.ts index c1f7aa3ee..6e0da79dc 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -103,6 +103,7 @@ export type StringValidation = | "time" | "duration" | "ip" + | "cidr" | "base64" | { includes: string; position?: number } | { startsWith: string } diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index f7037fcc2..ef5190062 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -372,6 +372,7 @@ test("checks getters", () => { expect(z.string().email().isUUID).toEqual(false); expect(z.string().email().isNANOID).toEqual(false); expect(z.string().email().isIP).toEqual(false); + expect(z.string().email().isCIDR).toEqual(false); expect(z.string().email().isULID).toEqual(false); expect(z.string().url().isEmail).toEqual(false); @@ -381,6 +382,7 @@ test("checks getters", () => { expect(z.string().url().isUUID).toEqual(false); expect(z.string().url().isNANOID).toEqual(false); expect(z.string().url().isIP).toEqual(false); + expect(z.string().url().isCIDR).toEqual(false); expect(z.string().url().isULID).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); @@ -390,6 +392,7 @@ test("checks getters", () => { expect(z.string().cuid().isUUID).toEqual(false); expect(z.string().cuid().isNANOID).toEqual(false); expect(z.string().cuid().isIP).toEqual(false); + expect(z.string().cuid().isCIDR).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); @@ -399,6 +402,7 @@ test("checks getters", () => { expect(z.string().cuid2().isUUID).toEqual(false); expect(z.string().cuid2().isNANOID).toEqual(false); expect(z.string().cuid2().isIP).toEqual(false); + expect(z.string().cuid2().isCIDR).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); @@ -408,6 +412,7 @@ test("checks getters", () => { expect(z.string().uuid().isUUID).toEqual(true); expect(z.string().uuid().isNANOID).toEqual(false); expect(z.string().uuid().isIP).toEqual(false); + expect(z.string().uuid().isCIDR).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); expect(z.string().nanoid().isEmail).toEqual(false); @@ -417,6 +422,7 @@ test("checks getters", () => { expect(z.string().nanoid().isUUID).toEqual(false); expect(z.string().nanoid().isNANOID).toEqual(true); expect(z.string().nanoid().isIP).toEqual(false); + expect(z.string().nanoid().isCIDR).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); expect(z.string().ip().isEmail).toEqual(false); @@ -426,8 +432,19 @@ test("checks getters", () => { expect(z.string().ip().isUUID).toEqual(false); expect(z.string().ip().isNANOID).toEqual(false); expect(z.string().ip().isIP).toEqual(true); + expect(z.string().ip().isCIDR).toEqual(false); expect(z.string().ip().isULID).toEqual(false); + expect(z.string().cidr().isEmail).toEqual(false); + expect(z.string().cidr().isURL).toEqual(false); + expect(z.string().cidr().isCUID).toEqual(false); + expect(z.string().cidr().isCUID2).toEqual(false); + expect(z.string().cidr().isUUID).toEqual(false); + expect(z.string().cidr().isNANOID).toEqual(false); + expect(z.string().cidr().isIP).toEqual(false); + expect(z.string().cidr().isCIDR).toEqual(true); + expect(z.string().cidr().isULID).toEqual(false); + expect(z.string().ulid().isEmail).toEqual(false); expect(z.string().ulid().isURL).toEqual(false); expect(z.string().ulid().isCUID).toEqual(false); @@ -435,6 +452,7 @@ test("checks getters", () => { expect(z.string().ulid().isUUID).toEqual(false); expect(z.string().ulid().isNANOID).toEqual(false); expect(z.string().ulid().isIP).toEqual(false); + expect(z.string().ulid().isCIDR).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); }); @@ -768,3 +786,45 @@ test("IP validation", () => { invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); }); + +test("CIDR validation", () => { + const ipv4Cidr = z.string().cidr({ version: "v4" }); + expect(() => ipv4Cidr.parse("2001:0db8:85a3::8a2e:0370:7334/64")).toThrow(); + + const ipv6Cidr = z.string().cidr({ version: "v6" }); + expect(() => ipv6Cidr.parse("192.168.0.1/24")).toThrow(); + + const validCidrs = [ + "192.168.0.0/24", + "10.0.0.0/8", + "203.0.113.0/24", + "192.0.2.0/24", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.1.0/24", + "fc00::/7", + "fd00::/8", + "2001:db8::/32", + "2607:f0d0:1002:51::4/64", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128", + "2001:0db8:1234:0000::/64", + ]; + + const invalidCidrs = [ + "192.168.1.1/33", + "10.0.0.1/-1", + "192.168.1.1/24/24", + "192.168.1.0/abc", + "2001:db8::1/129", + "2001:db8::1/-1", + "2001:db8::1/64/64", + "2001:db8::1/abc", + ]; + + // no parameters check IPv4 or IPv6 + const cidrSchema = z.string().cidr(); + expect(validCidrs.every((ip) => cidrSchema.safeParse(ip).success)).toBe(true); + expect( + invalidCidrs.every((ip) => cidrSchema.safeParse(ip).success === false) + ).toBe(true); +}); diff --git a/src/types.ts b/src/types.ts index be49c7012..df298ae3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -565,6 +565,7 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } + | { kind: "cidr"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string }; export interface ZodStringDef extends ZodTypeDef { @@ -608,11 +609,15 @@ let emojiRegex: RegExp; // faster, simpler, safer const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = + /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; // const ipv6Regex = // /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = + /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; // https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript const base64Regex = @@ -671,6 +676,17 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +function isValidCidr(ip: string, version?: IpVersion) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + + return false; +} + export class ZodString extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { @@ -933,6 +949,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "base64") { if (!base64Regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1006,6 +1032,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } + cidr(options?: string | { version?: IpVersion; message?: string }) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime( options?: | string @@ -1199,6 +1229,9 @@ export class ZodString extends ZodType { get isIP() { return !!this._def.checks.find((ch) => ch.kind === "ip"); } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } get isBase64() { return !!this._def.checks.find((ch) => ch.kind === "base64"); }