From af61c41ab8b2686c010091b83d2ef481fcfd3dc3 Mon Sep 17 00:00:00 2001 From: Luca De Santis <lucaroma2705@gmail.com> Date: Sun, 19 Feb 2023 19:16:08 +0100 Subject: [PATCH 1/5] Add base implementation of string ip validation For now check only IPv4 --- deno/lib/README.md | 12 +++++------ deno/lib/ZodError.ts | 1 + deno/lib/__tests__/string.test.ts | 26 ++++++++++++++++++++++ deno/lib/types.ts | 36 +++++++++++++++++++++++++------ src/ZodError.ts | 1 + src/__tests__/string.test.ts | 26 ++++++++++++++++++++++ src/types.ts | 36 +++++++++++++++++++++++++------ 7 files changed, 118 insertions(+), 20 deletions(-) diff --git a/deno/lib/README.md b/deno/lib/README.md index 6c2711ec5..065fca695 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -2349,14 +2349,14 @@ makeSchemaOptional(z.number()); Zod provides a subclass of Error called `ZodError`. ZodErrors contain an `issues` array containing detailed information about the validation problems. ```ts -const data = z +const result = z .object({ name: z.string(), }) .safeParse({ name: 12 }); -if (!data.success) { - data.error.issues; +if (!result.success) { + result.error.issues; /* [ { "code": "invalid_type", @@ -2378,14 +2378,14 @@ Zod's error reporting emphasizes _completeness_ and _correctness_. If you are lo You can use the `.format()` method to convert this error into a nested object. ```ts -const data = z +const result = z .object({ name: z.string(), }) .safeParse({ name: 12 }); -if (!data.success) { - const formatted = data.error.format(); +if (!result.success) { + const formatted = result.error.format(); /* { name: { _errors: [ 'Expected string, received number' ] } } */ diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index 365cb3cae..dcf4cc18a 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -97,6 +97,7 @@ export type StringValidation = | "cuid" | "cuid2" | "datetime" + | "ip" | { startsWith: string } | { endsWith: string }; diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 3df55f7b2..c85bd8422 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -230,30 +230,42 @@ test("checks getters", () => { expect(z.string().email().isCUID).toEqual(false); expect(z.string().email().isCUID2).toEqual(false); expect(z.string().email().isUUID).toEqual(false); + expect(z.string().email().isIP).toEqual(false); expect(z.string().url().isEmail).toEqual(false); expect(z.string().url().isURL).toEqual(true); expect(z.string().url().isCUID).toEqual(false); expect(z.string().url().isCUID2).toEqual(false); expect(z.string().url().isUUID).toEqual(false); + expect(z.string().url().isIP).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); expect(z.string().cuid().isURL).toEqual(false); expect(z.string().cuid().isCUID).toEqual(true); expect(z.string().cuid().isCUID2).toEqual(false); expect(z.string().cuid().isUUID).toEqual(false); + expect(z.string().cuid().isIP).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); expect(z.string().cuid2().isURL).toEqual(false); expect(z.string().cuid2().isCUID).toEqual(false); expect(z.string().cuid2().isCUID2).toEqual(true); expect(z.string().cuid2().isUUID).toEqual(false); + expect(z.string().cuid2().isIP).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); expect(z.string().uuid().isURL).toEqual(false); expect(z.string().uuid().isCUID).toEqual(false); expect(z.string().uuid().isCUID2).toEqual(false); expect(z.string().uuid().isUUID).toEqual(true); + expect(z.string().uuid().isIP).toEqual(false); + + expect(z.string().ip().isEmail).toEqual(false); + expect(z.string().ip().isURL).toEqual(false); + expect(z.string().ip().isCUID).toEqual(false); + expect(z.string().ip().isCUID2).toEqual(false); + expect(z.string().ip().isUUID).toEqual(false); + expect(z.string().ip().isIP).toEqual(true); }); test("min max getters", () => { @@ -359,3 +371,17 @@ test("datetime parsing", () => { datetimeOffset4Ms.parse("2020-10-14T17:42:29.124+00:00") ).toThrow(); }); + +test("IP validation", () => { + const ip = z.string().ip(); + + expect(ip.safeParse("192.168.1.1").success).toBe(true); + expect(ip.safeParse("255.255.255.255").success).toBe(true); + expect(ip.safeParse("0.0.0.0").success).toBe(true); + + expect(ip.safeParse("256.0.1.1").success).toBe(false); + expect(ip.safeParse("-1.53.78.1").success).toBe(false); + expect(ip.safeParse("0.0.0").success).toBe(false); + expect(ip.safeParse("128.44.1.0.5").success).toBe(false); + expect(ip.safeParse("1.1..1").success).toBe(false); +}) \ No newline at end of file diff --git a/deno/lib/types.ts b/deno/lib/types.ts index df6c1391d..8cc60c3ee 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -505,7 +505,8 @@ export type ZodStringCheck = offset: boolean; precision: number | null; message?: string; - }; + } + | { kind: "ip"; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -532,6 +533,9 @@ const emailRegex = const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|\uFE0E|\uFE0F)/; +const ipv4Regex = + /^(((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}))$/; + // Adapted from https://stackoverflow.com/a/3143231 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { if (args.precision) { @@ -750,6 +754,16 @@ export class ZodString extends ZodType<string, ZodStringDef> { }); status.dirty(); } + } else if (check.kind === "ip") { + if (!ipv4Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else { util.assertNever(check); } @@ -794,6 +808,11 @@ export class ZodString extends ZodType<string, ZodStringDef> { cuid2(message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); } + + ip(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) }); + } + datetime( options?: | string @@ -903,6 +922,9 @@ export class ZodString extends ZodType<string, ZodStringDef> { get isCUID2() { return !!this._def.checks.find((ch) => ch.kind === "cuid2"); } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } get minLength() { let min: number | null = null; @@ -3500,7 +3522,7 @@ export class ZodFunction< return this._def.returns; } - args<Items extends Parameters<typeof ZodTuple["create"]>[0]>( + args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>( ...items: Items ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> { return new ZodFunction({ @@ -4618,18 +4640,18 @@ const oboolean = () => booleanType().optional(); export const coerce = { string: ((arg) => - ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"], + ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"], number: ((arg) => - ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"], + ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"], boolean: ((arg) => ZodBoolean.create({ ...arg, coerce: true, - })) as typeof ZodBoolean["create"], + })) as (typeof ZodBoolean)["create"], bigint: ((arg) => - ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"], + ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"], date: ((arg) => - ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"], + ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"], }; export { diff --git a/src/ZodError.ts b/src/ZodError.ts index 03a34432a..0833ae614 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -97,6 +97,7 @@ export type StringValidation = | "cuid" | "cuid2" | "datetime" + | "ip" | { startsWith: string } | { endsWith: string }; diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 1f51004db..3f208d8e5 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -229,30 +229,42 @@ test("checks getters", () => { expect(z.string().email().isCUID).toEqual(false); expect(z.string().email().isCUID2).toEqual(false); expect(z.string().email().isUUID).toEqual(false); + expect(z.string().email().isIP).toEqual(false); expect(z.string().url().isEmail).toEqual(false); expect(z.string().url().isURL).toEqual(true); expect(z.string().url().isCUID).toEqual(false); expect(z.string().url().isCUID2).toEqual(false); expect(z.string().url().isUUID).toEqual(false); + expect(z.string().url().isIP).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); expect(z.string().cuid().isURL).toEqual(false); expect(z.string().cuid().isCUID).toEqual(true); expect(z.string().cuid().isCUID2).toEqual(false); expect(z.string().cuid().isUUID).toEqual(false); + expect(z.string().cuid().isIP).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); expect(z.string().cuid2().isURL).toEqual(false); expect(z.string().cuid2().isCUID).toEqual(false); expect(z.string().cuid2().isCUID2).toEqual(true); expect(z.string().cuid2().isUUID).toEqual(false); + expect(z.string().cuid2().isIP).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); expect(z.string().uuid().isURL).toEqual(false); expect(z.string().uuid().isCUID).toEqual(false); expect(z.string().uuid().isCUID2).toEqual(false); expect(z.string().uuid().isUUID).toEqual(true); + expect(z.string().uuid().isIP).toEqual(false); + + expect(z.string().ip().isEmail).toEqual(false); + expect(z.string().ip().isURL).toEqual(false); + expect(z.string().ip().isCUID).toEqual(false); + expect(z.string().ip().isCUID2).toEqual(false); + expect(z.string().ip().isUUID).toEqual(false); + expect(z.string().ip().isIP).toEqual(true); }); test("min max getters", () => { @@ -358,3 +370,17 @@ test("datetime parsing", () => { datetimeOffset4Ms.parse("2020-10-14T17:42:29.124+00:00") ).toThrow(); }); + +test("IP validation", () => { + const ip = z.string().ip(); + + expect(ip.safeParse("192.168.1.1").success).toBe(true); + expect(ip.safeParse("255.255.255.255").success).toBe(true); + expect(ip.safeParse("0.0.0.0").success).toBe(true); + + expect(ip.safeParse("256.0.1.1").success).toBe(false); + expect(ip.safeParse("-1.53.78.1").success).toBe(false); + expect(ip.safeParse("0.0.0").success).toBe(false); + expect(ip.safeParse("128.44.1.0.5").success).toBe(false); + expect(ip.safeParse("1.1..1").success).toBe(false); +}) \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 2af24c0a9..4a4f46665 100644 --- a/src/types.ts +++ b/src/types.ts @@ -505,7 +505,8 @@ export type ZodStringCheck = offset: boolean; precision: number | null; message?: string; - }; + } + | { kind: "ip"; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -532,6 +533,9 @@ const emailRegex = const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|\uFE0E|\uFE0F)/; +const ipv4Regex = + /^(((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}))$/; + // Adapted from https://stackoverflow.com/a/3143231 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { if (args.precision) { @@ -750,6 +754,16 @@ export class ZodString extends ZodType<string, ZodStringDef> { }); status.dirty(); } + } else if (check.kind === "ip") { + if (!ipv4Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else { util.assertNever(check); } @@ -794,6 +808,11 @@ export class ZodString extends ZodType<string, ZodStringDef> { cuid2(message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); } + + ip(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) }); + } + datetime( options?: | string @@ -903,6 +922,9 @@ export class ZodString extends ZodType<string, ZodStringDef> { get isCUID2() { return !!this._def.checks.find((ch) => ch.kind === "cuid2"); } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } get minLength() { let min: number | null = null; @@ -3500,7 +3522,7 @@ export class ZodFunction< return this._def.returns; } - args<Items extends Parameters<typeof ZodTuple["create"]>[0]>( + args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>( ...items: Items ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> { return new ZodFunction({ @@ -4618,18 +4640,18 @@ const oboolean = () => booleanType().optional(); export const coerce = { string: ((arg) => - ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"], + ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"], number: ((arg) => - ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"], + ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"], boolean: ((arg) => ZodBoolean.create({ ...arg, coerce: true, - })) as typeof ZodBoolean["create"], + })) as (typeof ZodBoolean)["create"], bigint: ((arg) => - ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"], + ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"], date: ((arg) => - ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"], + ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"], }; export { From d0cb77efc97c3a9b466c057b3f1a8a284d2f3aa5 Mon Sep 17 00:00:00 2001 From: Luca De Santis <lucaroma2705@gmail.com> Date: Sun, 19 Feb 2023 22:55:57 +0100 Subject: [PATCH 2/5] Add IP version If the version is not defined, the check goes for a valid IP whether it is version 4 or 6 --- deno/lib/__tests__/string.test.ts | 47 ++++++++++++++++++++++++------- deno/lib/types.ts | 25 +++++++++++++--- src/__tests__/string.test.ts | 47 ++++++++++++++++++++++++------- src/types.ts | 25 +++++++++++++--- 4 files changed, 116 insertions(+), 28 deletions(-) diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index c85bd8422..770bba9f5 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -374,14 +374,41 @@ test("datetime parsing", () => { test("IP validation", () => { const ip = z.string().ip(); + expect(ip.safeParse("122.122.122.122").success).toBe(true); + + const ipv4 = z.string().ip({ version: "v4" }); + expect(() => ipv4.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990")).toThrow(); + + const ipv6 = z.string().ip({ version: "v6" }); + expect(() => ipv6.parse("254.164.77.1")).toThrow(); + + /* For when IPv6 is implemented + const validIPs = [ + "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c", + "9d4:c956:420f:5788:4339:9b3b:2418:75c3", + "a6ea::2454:a5ce:94.105.123.75", + "474f:4c83::4e40:a47:ff95:0cda", + "d329:0:25b4:db47:a9d1:0:4926:0000", + "114.71.82.94", + "0.0.0.0", + "37.85.236.115", + ]; - expect(ip.safeParse("192.168.1.1").success).toBe(true); - expect(ip.safeParse("255.255.255.255").success).toBe(true); - expect(ip.safeParse("0.0.0.0").success).toBe(true); - - expect(ip.safeParse("256.0.1.1").success).toBe(false); - expect(ip.safeParse("-1.53.78.1").success).toBe(false); - expect(ip.safeParse("0.0.0").success).toBe(false); - expect(ip.safeParse("128.44.1.0.5").success).toBe(false); - expect(ip.safeParse("1.1..1").success).toBe(false); -}) \ No newline at end of file + const invalidIPs = [ + "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af", + "8f69::c757:395e:976e::3441", + "54cb::473f:d516:0.255.256.22", + "54cb::473f:d516:192.168.1", + "256.0.4.4", + "-1.0.555.4", + "0.0.0.0.0", + "1.1.1", + ]; + // no parameters check IPv4 or IPv6 + const ipSchema = z.string().ip(); + expect(validIPs.every((ip) => ipSchema.safeParse(ip).success)).toBe(true); + expect( + invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) + ).toBe(true); + */ +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 8cc60c3ee..1f3863d3a 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -486,6 +486,7 @@ export abstract class ZodType< ////////// ////////// ///////////////////////////////////////// ///////////////////////////////////////// +export type IpVersion = "v4" | "v6"; export type ZodStringCheck = | { kind: "min"; value: number; message?: string } | { kind: "max"; value: number; message?: string } @@ -506,7 +507,7 @@ export type ZodStringCheck = precision: number | null; message?: string; } - | { kind: "ip"; message?: string }; + | { kind: "ip"; version?: IpVersion; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -536,6 +537,8 @@ const emojiRegex = const ipv4Regex = /^(((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 = /not_implemented/; //! not implemented + // Adapted from https://stackoverflow.com/a/3143231 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { if (args.precision) { @@ -569,6 +572,17 @@ const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { } }; +function isValidIP(ip: string, version?: IpVersion) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + + return false; +} + export class ZodString extends ZodType<string, ZodStringDef> { _parse(input: ParseInput): ParseReturnType<string> { if (this._def.coerce) { @@ -755,7 +769,7 @@ export class ZodString extends ZodType<string, ZodStringDef> { status.dirty(); } } else if (check.kind === "ip") { - if (!ipv4Regex.test(input.data)) { + if (!isValidIP(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { validation: "ip", @@ -809,8 +823,11 @@ export class ZodString extends ZodType<string, ZodStringDef> { return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); } - ip(message?: errorUtil.ErrMessage) { - return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) }); + ip(options?: string | { version?: "v4" | "v6"; message?: string }) { + if (typeof options === "string") { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + return this._addCheck({ kind: "ip", ...options }); } datetime( diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 3f208d8e5..d10f9e716 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -373,14 +373,41 @@ test("datetime parsing", () => { test("IP validation", () => { const ip = z.string().ip(); + expect(ip.safeParse("122.122.122.122").success).toBe(true); + + const ipv4 = z.string().ip({ version: "v4" }); + expect(() => ipv4.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990")).toThrow(); + + const ipv6 = z.string().ip({ version: "v6" }); + expect(() => ipv6.parse("254.164.77.1")).toThrow(); + + /* For when IPv6 is implemented + const validIPs = [ + "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c", + "9d4:c956:420f:5788:4339:9b3b:2418:75c3", + "a6ea::2454:a5ce:94.105.123.75", + "474f:4c83::4e40:a47:ff95:0cda", + "d329:0:25b4:db47:a9d1:0:4926:0000", + "114.71.82.94", + "0.0.0.0", + "37.85.236.115", + ]; - expect(ip.safeParse("192.168.1.1").success).toBe(true); - expect(ip.safeParse("255.255.255.255").success).toBe(true); - expect(ip.safeParse("0.0.0.0").success).toBe(true); - - expect(ip.safeParse("256.0.1.1").success).toBe(false); - expect(ip.safeParse("-1.53.78.1").success).toBe(false); - expect(ip.safeParse("0.0.0").success).toBe(false); - expect(ip.safeParse("128.44.1.0.5").success).toBe(false); - expect(ip.safeParse("1.1..1").success).toBe(false); -}) \ No newline at end of file + const invalidIPs = [ + "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af", + "8f69::c757:395e:976e::3441", + "54cb::473f:d516:0.255.256.22", + "54cb::473f:d516:192.168.1", + "256.0.4.4", + "-1.0.555.4", + "0.0.0.0.0", + "1.1.1", + ]; + // no parameters check IPv4 or IPv6 + const ipSchema = z.string().ip(); + expect(validIPs.every((ip) => ipSchema.safeParse(ip).success)).toBe(true); + expect( + invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) + ).toBe(true); + */ +}); diff --git a/src/types.ts b/src/types.ts index 4a4f46665..6fd31d40b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -486,6 +486,7 @@ export abstract class ZodType< ////////// ////////// ///////////////////////////////////////// ///////////////////////////////////////// +export type IpVersion = "v4" | "v6"; export type ZodStringCheck = | { kind: "min"; value: number; message?: string } | { kind: "max"; value: number; message?: string } @@ -506,7 +507,7 @@ export type ZodStringCheck = precision: number | null; message?: string; } - | { kind: "ip"; message?: string }; + | { kind: "ip"; version?: IpVersion; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -536,6 +537,8 @@ const emojiRegex = const ipv4Regex = /^(((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 = /not_implemented/; //! not implemented + // Adapted from https://stackoverflow.com/a/3143231 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { if (args.precision) { @@ -569,6 +572,17 @@ const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { } }; +function isValidIP(ip: string, version?: IpVersion) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + + return false; +} + export class ZodString extends ZodType<string, ZodStringDef> { _parse(input: ParseInput): ParseReturnType<string> { if (this._def.coerce) { @@ -755,7 +769,7 @@ export class ZodString extends ZodType<string, ZodStringDef> { status.dirty(); } } else if (check.kind === "ip") { - if (!ipv4Regex.test(input.data)) { + if (!isValidIP(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { validation: "ip", @@ -809,8 +823,11 @@ export class ZodString extends ZodType<string, ZodStringDef> { return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); } - ip(message?: errorUtil.ErrMessage) { - return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) }); + ip(options?: string | { version?: "v4" | "v6"; message?: string }) { + if (typeof options === "string") { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + return this._addCheck({ kind: "ip", ...options }); } datetime( From 4349981fdc0fda95a0afecfe58053a3063a325dc Mon Sep 17 00:00:00 2001 From: Luca De Santis <lucaroma2705@gmail.com> Date: Sun, 19 Feb 2023 23:42:50 +0100 Subject: [PATCH 3/5] Add IP in docs --- README.md | 27 +++++++++++++++++++++++++++ deno/lib/README.md | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/README.md b/README.md index 065fca695..330bffd4b 100644 --- a/README.md +++ b/README.md @@ -594,6 +594,7 @@ z.string().startsWith(string); z.string().endsWith(string); z.string().trim(); // trim whitespace z.string().datetime(); // defaults to UTC, see below for options +z.string().ip(); // defaults to IPv4 and IPv6, see below for options ``` > Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions that can be used in conjunction with [Refinements](#refine). @@ -619,6 +620,7 @@ z.string().uuid({ message: "Invalid UUID" }); z.string().startsWith("https://", { message: "Must provide secure URL" }); z.string().endsWith(".com", { message: "Only .com domains allowed" }); z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); +z.string().ip({ message: "Invalid IP address" }); ``` ### Datetime validation @@ -656,6 +658,31 @@ datetime.parse("2020-01-01T00:00:00Z"); // fail datetime.parse("2020-01-01T00:00:00.123456Z"); // fail ``` +### IP address validation + +The `z.string().ip()` method by default validate IPv4 and IPv6. + +```ts +const ip = z.string().ip(); + +ip.parse("192.168.1.1"); // pass +ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // pass +ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1"); // pass + +ip.parse("256.1.1.1"); // fail +ip.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003"); // fail +``` + +You can additionally set the IP `version`. + +```ts +const ipv4 = z.string().ip({ version: "v4" }); +ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail + +const ipv6 = z.string().ip({ version: "v6" }); +ipv6.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 065fca695..330bffd4b 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -594,6 +594,7 @@ z.string().startsWith(string); z.string().endsWith(string); z.string().trim(); // trim whitespace z.string().datetime(); // defaults to UTC, see below for options +z.string().ip(); // defaults to IPv4 and IPv6, see below for options ``` > Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions that can be used in conjunction with [Refinements](#refine). @@ -619,6 +620,7 @@ z.string().uuid({ message: "Invalid UUID" }); z.string().startsWith("https://", { message: "Must provide secure URL" }); z.string().endsWith(".com", { message: "Only .com domains allowed" }); z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); +z.string().ip({ message: "Invalid IP address" }); ``` ### Datetime validation @@ -656,6 +658,31 @@ datetime.parse("2020-01-01T00:00:00Z"); // fail datetime.parse("2020-01-01T00:00:00.123456Z"); // fail ``` +### IP address validation + +The `z.string().ip()` method by default validate IPv4 and IPv6. + +```ts +const ip = z.string().ip(); + +ip.parse("192.168.1.1"); // pass +ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // pass +ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1"); // pass + +ip.parse("256.1.1.1"); // fail +ip.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003"); // fail +``` + +You can additionally set the IP `version`. + +```ts +const ipv4 = z.string().ip({ version: "v4" }); +ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail + +const ipv6 = z.string().ip({ version: "v6" }); +ipv6.parse("192.168.1.1"); // fail +``` + ## Numbers You can customize certain error messages when creating a number schema. From 78a2eee225f1194ce6c5ff92829cd44379a70f7a Mon Sep 17 00:00:00 2001 From: Luca De Santis <lucaroma2705@gmail.com> Date: Tue, 21 Feb 2023 16:39:48 +0100 Subject: [PATCH 4/5] Implement IPv6 validation --- deno/lib/__tests__/string.test.ts | 4 ++-- deno/lib/types.ts | 3 ++- src/__tests__/string.test.ts | 4 ++-- src/types.ts | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 770bba9f5..b2b6437af 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -382,13 +382,13 @@ test("IP validation", () => { const ipv6 = z.string().ip({ version: "v6" }); expect(() => ipv6.parse("254.164.77.1")).toThrow(); - /* For when IPv6 is implemented const validIPs = [ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c", "9d4:c956:420f:5788:4339:9b3b:2418:75c3", "a6ea::2454:a5ce:94.105.123.75", "474f:4c83::4e40:a47:ff95:0cda", "d329:0:25b4:db47:a9d1:0:4926:0000", + "e48:10fb:1499:3e28:e4b6:dea5:4692:912c", "114.71.82.94", "0.0.0.0", "37.85.236.115", @@ -396,6 +396,7 @@ test("IP validation", () => { const invalidIPs = [ "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af", + "d5e7:7214:2b78::3906:85e6:53cc:709:32ba", "8f69::c757:395e:976e::3441", "54cb::473f:d516:0.255.256.22", "54cb::473f:d516:192.168.1", @@ -410,5 +411,4 @@ test("IP validation", () => { expect( invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); - */ }); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 1f3863d3a..5e759477b 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -537,7 +537,8 @@ const emojiRegex = const ipv4Regex = /^(((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 = /not_implemented/; //! not implemented +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})))$/; // Adapted from https://stackoverflow.com/a/3143231 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index d10f9e716..39a54da2b 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -381,13 +381,13 @@ test("IP validation", () => { const ipv6 = z.string().ip({ version: "v6" }); expect(() => ipv6.parse("254.164.77.1")).toThrow(); - /* For when IPv6 is implemented const validIPs = [ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c", "9d4:c956:420f:5788:4339:9b3b:2418:75c3", "a6ea::2454:a5ce:94.105.123.75", "474f:4c83::4e40:a47:ff95:0cda", "d329:0:25b4:db47:a9d1:0:4926:0000", + "e48:10fb:1499:3e28:e4b6:dea5:4692:912c", "114.71.82.94", "0.0.0.0", "37.85.236.115", @@ -395,6 +395,7 @@ test("IP validation", () => { const invalidIPs = [ "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af", + "d5e7:7214:2b78::3906:85e6:53cc:709:32ba", "8f69::c757:395e:976e::3441", "54cb::473f:d516:0.255.256.22", "54cb::473f:d516:192.168.1", @@ -409,5 +410,4 @@ test("IP validation", () => { expect( invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); - */ }); diff --git a/src/types.ts b/src/types.ts index 6fd31d40b..1f9248154 100644 --- a/src/types.ts +++ b/src/types.ts @@ -537,7 +537,8 @@ const emojiRegex = const ipv4Regex = /^(((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 = /not_implemented/; //! not implemented +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})))$/; // Adapted from https://stackoverflow.com/a/3143231 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => { From 1808f47f3b7af5959339c808bf151b3bef05099a Mon Sep 17 00:00:00 2001 From: Colin McDonnell <colinmcd94@gmail.com> Date: Sun, 26 Feb 2023 13:14:58 -0800 Subject: [PATCH 5/5] Use errToObj and update readme --- README.md | 4 +++- deno/lib/README.md | 4 +++- deno/lib/types.ts | 17 +++++++---------- src/types.ts | 17 +++++++---------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 330bffd4b..923ba6aca 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ - [Coercion for primitives](#coercion-for-primitives) - [Literals](#literals) - [Strings](#strings) + - [Datetime](#datetime-validation) + - [IP](#ip-address-validation) - [Numbers](#numbers) - [NaNs](#nans) - [Booleans](#booleans) @@ -677,7 +679,7 @@ You can additionally set the IP `version`. ```ts const ipv4 = z.string().ip({ version: "v4" }); -ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail +ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail diff --git a/deno/lib/README.md b/deno/lib/README.md index 330bffd4b..923ba6aca 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -55,6 +55,8 @@ - [Coercion for primitives](#coercion-for-primitives) - [Literals](#literals) - [Strings](#strings) + - [Datetime](#datetime-validation) + - [IP](#ip-address-validation) - [Numbers](#numbers) - [NaNs](#nans) - [Booleans](#booleans) @@ -677,7 +679,7 @@ You can additionally set the IP `version`. ```ts const ipv4 = z.string().ip({ version: "v4" }); -ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail +ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 5e759477b..ac8523fb5 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -825,10 +825,7 @@ export class ZodString extends ZodType<string, ZodStringDef> { } ip(options?: string | { version?: "v4" | "v6"; message?: string }) { - if (typeof options === "string") { - return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); - } - return this._addCheck({ kind: "ip", ...options }); + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } datetime( @@ -3540,7 +3537,7 @@ export class ZodFunction< return this._def.returns; } - args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>( + args<Items extends Parameters<typeof ZodTuple["create"]>[0]>( ...items: Items ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> { return new ZodFunction({ @@ -4658,18 +4655,18 @@ const oboolean = () => booleanType().optional(); export const coerce = { string: ((arg) => - ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"], + ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"], number: ((arg) => - ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"], + ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"], boolean: ((arg) => ZodBoolean.create({ ...arg, coerce: true, - })) as (typeof ZodBoolean)["create"], + })) as typeof ZodBoolean["create"], bigint: ((arg) => - ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"], + ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"], date: ((arg) => - ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"], + ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"], }; export { diff --git a/src/types.ts b/src/types.ts index 1f9248154..8a66c85a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -825,10 +825,7 @@ export class ZodString extends ZodType<string, ZodStringDef> { } ip(options?: string | { version?: "v4" | "v6"; message?: string }) { - if (typeof options === "string") { - return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); - } - return this._addCheck({ kind: "ip", ...options }); + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } datetime( @@ -3540,7 +3537,7 @@ export class ZodFunction< return this._def.returns; } - args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>( + args<Items extends Parameters<typeof ZodTuple["create"]>[0]>( ...items: Items ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> { return new ZodFunction({ @@ -4658,18 +4655,18 @@ const oboolean = () => booleanType().optional(); export const coerce = { string: ((arg) => - ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"], + ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"], number: ((arg) => - ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"], + ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"], boolean: ((arg) => ZodBoolean.create({ ...arg, coerce: true, - })) as (typeof ZodBoolean)["create"], + })) as typeof ZodBoolean["create"], bigint: ((arg) => - ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"], + ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"], date: ((arg) => - ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"], + ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"], }; export {