From c409670015e139ed38b1b7cfc06f17cdb279493a Mon Sep 17 00:00:00 2001
From: wataryooou
Date: Sat, 14 Sep 2024 19:03:23 +0900
Subject: [PATCH 1/7] feat: add ip range validation
---
deno/lib/README.md | 35 ++++++++++++++-
deno/lib/ZodError.ts | 1 +
deno/lib/__tests__/string.test.ts | 74 +++++++++++++++++++++++++++++++
deno/lib/types.ts | 53 ++++++++++++++++++++++
src/ZodError.ts | 1 +
src/types.ts | 53 ++++++++++++++++++++++
6 files changed, 215 insertions(+), 2 deletions(-)
diff --git a/deno/lib/README.md b/deno/lib/README.md
index 91d97beb1..3abda6463 100644
--- a/deno/lib/README.md
+++ b/deno/lib/README.md
@@ -13,7 +13,7 @@
-
+
@@ -76,6 +76,7 @@
- [Dates](#dates)
- [Times](#times)
- [IP addresses](#ip-addresses)
+ - [IP addresses range](#ip-addresses-range)
- [Numbers](#numbers)
- [BigInts](#bigints)
- [NaNs](#nans)
@@ -219,7 +220,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
@@ -806,6 +807,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().ipRange({ message: "Invalid IP address range" });
```
### Datetimes
@@ -911,6 +913,35 @@ const ipv6 = z.string().ip({ version: "v6" });
ipv6.parse("192.168.1.1"); // fail
```
+### IP addresses range
+
+The `z.string().ipRange()` method by default validate IPv4 and IPv6.
+
+```ts
+const ipRange = z.string().ipRange();
+
+ipRange.parse("192.168.1.1/32"); // pass
+ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // pass
+ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1/64"); // pass
+
+ipRange.parse("256.1.1.1/32"); // fail
+ipRange.parse("192.168.1.1/33"); // fail
+ipRange.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003/128"); // fail
+ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/129"); // fail
+```
+
+You can additionally set the IP `version`.
+
+```ts
+const ipv4Range = z.string().ipRange({ version: "v4" });
+ipv4Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // fail
+ipv4Range.parse("192.168.1.1/128"); // fail
+
+const ipv6Range = z.string().ipRange({ version: "v6" });
+ipv6Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/32"); // fail
+ipv6Range.parse("192.168.1.1/128"); // 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..906aac8e2 100644
--- a/deno/lib/ZodError.ts
+++ b/deno/lib/ZodError.ts
@@ -103,6 +103,7 @@ export type StringValidation =
| "time"
| "duration"
| "ip"
+ | "ipRange"
| "base64"
| { includes: string; position?: number }
| { startsWith: string }
diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index b70152750..dd9232100 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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).toEqual(false);
expect(z.string().ip().isULID).toEqual(false);
+ expect(z.string().ipRange().isEmail).toEqual(false);
+ expect(z.string().ipRange().isURL).toEqual(false);
+ expect(z.string().ipRange().isCUID).toEqual(false);
+ expect(z.string().ipRange().isCUID2).toEqual(false);
+ expect(z.string().ipRange().isUUID).toEqual(false);
+ expect(z.string().ipRange().isNANOID).toEqual(false);
+ expect(z.string().ipRange().isIP).toEqual(false);
+ expect(z.string().ipRange().isIPRange).toEqual(true);
+ expect(z.string().ipRange().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().isIPRange).toEqual(false);
expect(z.string().ulid().isULID).toEqual(true);
});
@@ -767,3 +785,59 @@ test("IP validation", () => {
invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false)
).toBe(true);
});
+
+test("IP Range validation", () => {
+ const ipRange = z.string().ipRange();
+ expect(ipRange.safeParse("122.122.122.122/0").success).toBe(true);
+ expect(ipRange.safeParse("122.122.122.122/32").success).toBe(true);
+ expect(ipRange.safeParse("122.122.122.122/-1").success).toBe(false);
+ expect(ipRange.safeParse("122.122.122.122/33").success).toBe(false);
+
+ const ipv4Range = z.string().ipRange({ version: "v4" });
+ expect(() =>
+ ipv4Range.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990/128")
+ ).toThrow();
+
+ const ipv6Range = z.string().ipRange({ version: "v6" });
+ expect(() => ipv6Range.parse("254.164.77.1/32")).toThrow();
+
+ const validIPRanges = [
+ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/0",
+ "9d4:c956:420f:5788:4339:9b3b:2418:75c3/128",
+ "a6ea::2454:a5ce:94.105.123.75/32",
+ "474f:4c83::4e40:a47:ff95:0cda/16",
+ "d329:0:25b4:db47:a9d1:0:4926:0000/64",
+ "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/8",
+ "114.71.82.94/0",
+ "0.0.0.0/0",
+ "37.85.236.115/32",
+ ];
+
+ const invalidIPRanges = [
+ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/129",
+ "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af/128",
+ "474f:4c83::4e40:a47:ff95:0cda/129",
+ "8f69::c757:395e:976e::3441/64",
+ "54cb::473f:d516:0.255.256.22/64",
+ "54cb::473f:d516:192.168.1/64",
+ "474f:4c83::4e40:a47:ff95:0cda/129",
+ "d329:0:25b4:db47:a9d1:0:4926:0000/129",
+ "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/129",
+ "256.0.4.4/16",
+ "-1.0.555.4/32",
+ "0.0.0.0.0/0",
+ "1.1.1/16",
+ "114.71.82.94/128",
+ "0.0.0.0/33",
+ ];
+ // no parameters check IPv4 or IPv6
+ const ipRangeSchema = z.string().ipRange();
+ expect(
+ validIPRanges.every((ipRange) => ipRangeSchema.safeParse(ipRange).success)
+ ).toBe(true);
+ expect(
+ invalidIPRanges.every(
+ (ipRange) => ipRangeSchema.safeParse(ipRange).success === false
+ )
+ ).toBe(true);
+});
diff --git a/deno/lib/types.ts b/deno/lib/types.ts
index d634e524f..4dc5412a5 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: "ipRange"; version?: IpVersion; message?: string }
| { kind: "base64"; message?: string };
export interface ZodStringDef extends ZodTypeDef {
@@ -608,9 +609,11 @@ 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 = /^(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 ipv6CidrRegex = /^(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 =
@@ -669,6 +672,36 @@ function isValidIP(ip: string, version?: IpVersion) {
return false;
}
+function isValidIPRange(ip: string, version?: IpVersion) {
+ const [ipAddress, mask] = ip.split("/");
+ if (ipAddress === undefined || mask === undefined) {
+ return false;
+ }
+ if (
+ !version &&
+ ((ipv4Regex.test(ipAddress) && ipv4CidrRegex.test(mask)) ||
+ (ipv6Regex.test(ipAddress) && ipv6CidrRegex.test(mask)))
+ ) {
+ return true;
+ }
+ if (
+ version === "v4" &&
+ ipv4Regex.test(ipAddress) &&
+ ipv4CidrRegex.test(mask)
+ ) {
+ return true;
+ }
+ if (
+ version === "v6" &&
+ ipv6Regex.test(ipAddress) &&
+ ipv6CidrRegex.test(mask)
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
export class ZodString extends ZodType
{
_parse(input: ParseInput): ParseReturnType {
if (this._def.coerce) {
@@ -931,6 +964,16 @@ export class ZodString extends ZodType {
});
status.dirty();
}
+ } else if (check.kind === "ipRange") {
+ if (!isValidIPRange(input.data, check.version)) {
+ ctx = this._getOrReturnCtx(input, ctx);
+ addIssueToContext(ctx, {
+ validation: "ipRange",
+ code: ZodIssueCode.invalid_string,
+ message: check.message,
+ });
+ status.dirty();
+ }
} else if (check.kind === "base64") {
if (!base64Regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
@@ -1004,6 +1047,13 @@ export class ZodString extends ZodType {
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
}
+ ipRange(options?: string | { version?: IpVersion; message?: string }) {
+ return this._addCheck({
+ kind: "ipRange",
+ ...errorUtil.errToObj(options),
+ });
+ }
+
datetime(
options?:
| string
@@ -1197,6 +1247,9 @@ export class ZodString extends ZodType {
get isIP() {
return !!this._def.checks.find((ch) => ch.kind === "ip");
}
+ get isIPRange() {
+ return !!this._def.checks.find((ch) => ch.kind === "ipRange");
+ }
get isBase64() {
return !!this._def.checks.find((ch) => ch.kind === "base64");
}
diff --git a/src/ZodError.ts b/src/ZodError.ts
index c1f7aa3ee..d9c6df2e7 100644
--- a/src/ZodError.ts
+++ b/src/ZodError.ts
@@ -103,6 +103,7 @@ export type StringValidation =
| "time"
| "duration"
| "ip"
+ | "ipRange"
| "base64"
| { includes: string; position?: number }
| { startsWith: string }
diff --git a/src/types.ts b/src/types.ts
index 0767073c5..85bc7efe3 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: "ipRange"; version?: IpVersion; message?: string }
| { kind: "base64"; message?: string };
export interface ZodStringDef extends ZodTypeDef {
@@ -608,9 +609,11 @@ 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 = /^(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 ipv6CidrRegex = /^(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 =
@@ -669,6 +672,36 @@ function isValidIP(ip: string, version?: IpVersion) {
return false;
}
+function isValidIPRange(ip: string, version?: IpVersion) {
+ const [ipAddress, mask] = ip.split("/");
+ if (ipAddress === undefined || mask === undefined) {
+ return false;
+ }
+ if (
+ !version &&
+ ((ipv4Regex.test(ipAddress) && ipv4CidrRegex.test(mask)) ||
+ (ipv6Regex.test(ipAddress) && ipv6CidrRegex.test(mask)))
+ ) {
+ return true;
+ }
+ if (
+ version === "v4" &&
+ ipv4Regex.test(ipAddress) &&
+ ipv4CidrRegex.test(mask)
+ ) {
+ return true;
+ }
+ if (
+ version === "v6" &&
+ ipv6Regex.test(ipAddress) &&
+ ipv6CidrRegex.test(mask)
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
export class ZodString extends ZodType {
_parse(input: ParseInput): ParseReturnType {
if (this._def.coerce) {
@@ -931,6 +964,16 @@ export class ZodString extends ZodType {
});
status.dirty();
}
+ } else if (check.kind === "ipRange") {
+ if (!isValidIPRange(input.data, check.version)) {
+ ctx = this._getOrReturnCtx(input, ctx);
+ addIssueToContext(ctx, {
+ validation: "ipRange",
+ code: ZodIssueCode.invalid_string,
+ message: check.message,
+ });
+ status.dirty();
+ }
} else if (check.kind === "base64") {
if (!base64Regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
@@ -1004,6 +1047,13 @@ export class ZodString extends ZodType {
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
}
+ ipRange(options?: string | { version?: IpVersion; message?: string }) {
+ return this._addCheck({
+ kind: "ipRange",
+ ...errorUtil.errToObj(options),
+ });
+ }
+
datetime(
options?:
| string
@@ -1197,6 +1247,9 @@ export class ZodString extends ZodType {
get isIP() {
return !!this._def.checks.find((ch) => ch.kind === "ip");
}
+ get isIPRange() {
+ return !!this._def.checks.find((ch) => ch.kind === "ipRange");
+ }
get isBase64() {
return !!this._def.checks.find((ch) => ch.kind === "base64");
}
From bd5dde8825e8c2c3a764cd4eaf5284f4aeba6a44 Mon Sep 17 00:00:00 2001
From: wataryooou
Date: Sat, 14 Sep 2024 19:03:49 +0900
Subject: [PATCH 2/7] test: add ip range validation test
---
src/__tests__/string.test.ts | 74 ++++++++++++++++++++++++++++++++++++
1 file changed, 74 insertions(+)
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index 24f10ee45..cc4419e12 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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).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().isIPRange).toEqual(false);
expect(z.string().ip().isULID).toEqual(false);
+ expect(z.string().ipRange().isEmail).toEqual(false);
+ expect(z.string().ipRange().isURL).toEqual(false);
+ expect(z.string().ipRange().isCUID).toEqual(false);
+ expect(z.string().ipRange().isCUID2).toEqual(false);
+ expect(z.string().ipRange().isUUID).toEqual(false);
+ expect(z.string().ipRange().isNANOID).toEqual(false);
+ expect(z.string().ipRange().isIP).toEqual(false);
+ expect(z.string().ipRange().isIPRange).toEqual(true);
+ expect(z.string().ipRange().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().isIPRange).toEqual(false);
expect(z.string().ulid().isULID).toEqual(true);
});
@@ -766,3 +784,59 @@ test("IP validation", () => {
invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false)
).toBe(true);
});
+
+test("IP Range validation", () => {
+ const ipRange = z.string().ipRange();
+ expect(ipRange.safeParse("122.122.122.122/0").success).toBe(true);
+ expect(ipRange.safeParse("122.122.122.122/32").success).toBe(true);
+ expect(ipRange.safeParse("122.122.122.122/-1").success).toBe(false);
+ expect(ipRange.safeParse("122.122.122.122/33").success).toBe(false);
+
+ const ipv4Range = z.string().ipRange({ version: "v4" });
+ expect(() =>
+ ipv4Range.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990/128")
+ ).toThrow();
+
+ const ipv6Range = z.string().ipRange({ version: "v6" });
+ expect(() => ipv6Range.parse("254.164.77.1/32")).toThrow();
+
+ const validIPRanges = [
+ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/0",
+ "9d4:c956:420f:5788:4339:9b3b:2418:75c3/128",
+ "a6ea::2454:a5ce:94.105.123.75/32",
+ "474f:4c83::4e40:a47:ff95:0cda/16",
+ "d329:0:25b4:db47:a9d1:0:4926:0000/64",
+ "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/8",
+ "114.71.82.94/0",
+ "0.0.0.0/0",
+ "37.85.236.115/32",
+ ];
+
+ const invalidIPRanges = [
+ "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/129",
+ "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af/128",
+ "474f:4c83::4e40:a47:ff95:0cda/129",
+ "8f69::c757:395e:976e::3441/64",
+ "54cb::473f:d516:0.255.256.22/64",
+ "54cb::473f:d516:192.168.1/64",
+ "474f:4c83::4e40:a47:ff95:0cda/129",
+ "d329:0:25b4:db47:a9d1:0:4926:0000/129",
+ "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/129",
+ "256.0.4.4/16",
+ "-1.0.555.4/32",
+ "0.0.0.0.0/0",
+ "1.1.1/16",
+ "114.71.82.94/128",
+ "0.0.0.0/33",
+ ];
+ // no parameters check IPv4 or IPv6
+ const ipRangeSchema = z.string().ipRange();
+ expect(
+ validIPRanges.every((ipRange) => ipRangeSchema.safeParse(ipRange).success)
+ ).toBe(true);
+ expect(
+ invalidIPRanges.every(
+ (ipRange) => ipRangeSchema.safeParse(ipRange).success === false
+ )
+ ).toBe(true);
+});
From fba8c667766cdeeca6af79ee6c57eebb5504ec2f Mon Sep 17 00:00:00 2001
From: wataryooou
Date: Sat, 14 Sep 2024 19:04:07 +0900
Subject: [PATCH 3/7] docs: add ip range validation in docs
---
README.md | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/README.md b/README.md
index a721693b1..3abda6463 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@
- [Dates](#dates)
- [Times](#times)
- [IP addresses](#ip-addresses)
+ - [IP addresses range](#ip-addresses-range)
- [Numbers](#numbers)
- [BigInts](#bigints)
- [NaNs](#nans)
@@ -806,6 +807,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().ipRange({ message: "Invalid IP address range" });
```
### Datetimes
@@ -911,6 +913,35 @@ const ipv6 = z.string().ip({ version: "v6" });
ipv6.parse("192.168.1.1"); // fail
```
+### IP addresses range
+
+The `z.string().ipRange()` method by default validate IPv4 and IPv6.
+
+```ts
+const ipRange = z.string().ipRange();
+
+ipRange.parse("192.168.1.1/32"); // pass
+ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // pass
+ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1/64"); // pass
+
+ipRange.parse("256.1.1.1/32"); // fail
+ipRange.parse("192.168.1.1/33"); // fail
+ipRange.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003/128"); // fail
+ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/129"); // fail
+```
+
+You can additionally set the IP `version`.
+
+```ts
+const ipv4Range = z.string().ipRange({ version: "v4" });
+ipv4Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // fail
+ipv4Range.parse("192.168.1.1/128"); // fail
+
+const ipv6Range = z.string().ipRange({ version: "v6" });
+ipv6Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/32"); // fail
+ipv6Range.parse("192.168.1.1/128"); // fail
+```
+
## Numbers
You can customize certain error messages when creating a number schema.
From 68a379c30b9a736d9829f9f271557f5ea25014d9 Mon Sep 17 00:00:00 2001
From: wataryooou
Date: Sat, 14 Sep 2024 19:27:05 +0900
Subject: [PATCH 4/7] docs: fix typo
---
README.md | 2 +-
deno/lib/README.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 3abda6463..1de067936 100644
--- a/README.md
+++ b/README.md
@@ -939,7 +939,7 @@ ipv4Range.parse("192.168.1.1/128"); // fail
const ipv6Range = z.string().ipRange({ version: "v6" });
ipv6Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/32"); // fail
-ipv6Range.parse("192.168.1.1/128"); // fail
+ipv6Range.parse("192.168.1.1/32"); // fail
```
## Numbers
diff --git a/deno/lib/README.md b/deno/lib/README.md
index 3abda6463..1de067936 100644
--- a/deno/lib/README.md
+++ b/deno/lib/README.md
@@ -939,7 +939,7 @@ ipv4Range.parse("192.168.1.1/128"); // fail
const ipv6Range = z.string().ipRange({ version: "v6" });
ipv6Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/32"); // fail
-ipv6Range.parse("192.168.1.1/128"); // fail
+ipv6Range.parse("192.168.1.1/32"); // fail
```
## Numbers
From 189c1999402c310ea81994d6277b24c5ac8a3810 Mon Sep 17 00:00:00 2001
From: wataryooou
Date: Sat, 19 Oct 2024 15:56:48 +0900
Subject: [PATCH 5/7] fix: failed test
---
deno/lib/README.md | 4 ++++
deno/lib/__tests__/string.test.ts | 1 -
src/__tests__/string.test.ts | 1 -
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/deno/lib/README.md b/deno/lib/README.md
index a721693b1..cdae9403c 100644
--- a/deno/lib/README.md
+++ b/deno/lib/README.md
@@ -491,6 +491,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`sveltekit-superforms`](https://github.com/ciscoheat/sveltekit-superforms): Supercharged form library for SvelteKit with Zod validation.
- [`mobx-zod-form`](https://github.com/MonoidDev/mobx-zod-form): Data-first form builder based on MobX & Zod.
- [`@vee-validate/zod`](https://github.com/logaretm/vee-validate/tree/main/packages/zod): Form library for Vue.js with Zod schema validation.
+- [`zod-form-renderer`](https://github.com/thepeaklab/zod-form-renderer): Auto-infer form fields from zod schema and render them with react-hook-form with E2E type safety.
#### Zod to X
@@ -505,6 +506,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`zod-openapi`](https://github.com/samchungy/zod-openapi): Create full OpenAPI v3.x documentation from Zod schemas.
- [`fastify-zod-openapi`](https://github.com/samchungy/fastify-zod-openapi): Fastify type provider, validation, serialization and @fastify/swagger support for Zod schemas.
- [`typeschema`](https://typeschema.com/): Universal adapter for schema validation.
+- [`zodex`](https://github.com/commonbaseapp/zodex): (De)serialization for zod schemas
#### X to Zod
@@ -538,11 +540,13 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`freerstore`](https://github.com/JacobWeisenburger/freerstore): Firestore cost optimizer.
- [`slonik`](https://github.com/gajus/slonik/tree/gajus/add-zod-validation-backwards-compatible#runtime-validation-and-static-type-inference): Node.js Postgres client with strong Zod integration.
+- [`schemql`](https://github.com/a2lix/schemql): Enhances your SQL workflow by combining raw SQL with targeted type safety and schema validation.
- [`soly`](https://github.com/mdbetancourt/soly): Create CLI applications with zod.
- [`pastel`](https://github.com/vadimdemedes/pastel): Create CLI applications with react, zod, and ink.
- [`zod-xlsx`](https://github.com/sidwebworks/zod-xlsx): A xlsx based resource validator using Zod schemas.
- [`znv`](https://github.com/lostfictions/znv): Type-safe environment parsing and validation for Node.js with Zod schemas.
- [`zod-config`](https://github.com/alexmarqs/zod-config): Load configurations across multiple sources with flexible adapters, ensuring type safety with Zod.
+- [`unplugin-environment`](https://github.com/r17x/js/tree/main/packages/unplugin-environment#readme): A plugin for loading enviroment variables safely with schema validation, simple with virtual module, type-safe with intellisense, and better DX 🔥 🚀 👷. Powered by Zod.
#### Utilities for Zod
diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index 564fa84d6..64438717a 100644
--- a/deno/lib/__tests__/string.test.ts
+++ b/deno/lib/__tests__/string.test.ts
@@ -740,7 +740,6 @@ test("IP validation", () => {
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",
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index 368dace7f..f7037fcc2 100644
--- a/src/__tests__/string.test.ts
+++ b/src/__tests__/string.test.ts
@@ -739,7 +739,6 @@ test("IP validation", () => {
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",
From db35eaa59ba4d9726014af2c77810f580854dd79 Mon Sep 17 00:00:00 2001
From: wataryooou
Date: Sat, 19 Oct 2024 16:12:09 +0900
Subject: [PATCH 6/7] fix: test
---
deno/lib/__tests__/string.test.ts | 1 -
src/__tests__/string.test.ts | 1 -
2 files changed, 2 deletions(-)
diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index 41c4d66d2..6e0b2a05a 100644
--- a/deno/lib/__tests__/string.test.ts
+++ b/deno/lib/__tests__/string.test.ts
@@ -806,7 +806,6 @@ test("IP Range validation", () => {
const validIPRanges = [
"1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/0",
"9d4:c956:420f:5788:4339:9b3b:2418:75c3/128",
- "a6ea::2454:a5ce:94.105.123.75/32",
"474f:4c83::4e40:a47:ff95:0cda/16",
"d329:0:25b4:db47:a9d1:0:4926:0000/64",
"e48:10fb:1499:3e28:e4b6:dea5:4692:912c/8",
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index e3897507a..5d59969de 100644
--- a/src/__tests__/string.test.ts
+++ b/src/__tests__/string.test.ts
@@ -805,7 +805,6 @@ test("IP Range validation", () => {
const validIPRanges = [
"1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/0",
"9d4:c956:420f:5788:4339:9b3b:2418:75c3/128",
- "a6ea::2454:a5ce:94.105.123.75/32",
"474f:4c83::4e40:a47:ff95:0cda/16",
"d329:0:25b4:db47:a9d1:0:4926:0000/64",
"e48:10fb:1499:3e28:e4b6:dea5:4692:912c/8",
From 45428a3959375fe8a8b5eaa82902ed78ba7dc313 Mon Sep 17 00:00:00 2001
From: wataryooou
Date: Sun, 27 Oct 2024 17:09:28 +0900
Subject: [PATCH 7/7] refactor
---
README.md | 29 ++-----
deno/lib/README.md | 29 ++-----
deno/lib/__tests__/string.test.ts | 123 +++++++++++++++++-------------
deno/lib/types.ts | 80 ++++++++++++-------
src/__tests__/string.test.ts | 123 +++++++++++++++++-------------
src/types.ts | 80 ++++++++++++-------
6 files changed, 260 insertions(+), 204 deletions(-)
diff --git a/README.md b/README.md
index 1a2da4f26..0c5b42834 100644
--- a/README.md
+++ b/README.md
@@ -811,7 +811,6 @@ 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().ipRange({ message: "Invalid IP address range" });
```
### Datetimes
@@ -919,31 +918,19 @@ ipv6.parse("192.168.1.1"); // fail
### IP addresses range
-The `z.string().ipRange()` method by default validate IPv4 and IPv6.
+The `z.string().ipRange()` method checks if a given IP address falls within a specified CIDR range.
```ts
-const ipRange = z.string().ipRange();
+const ipv4Range = z.string().ipRange({ cidr: "192.168.1.0/24", version: "v4" });
-ipRange.parse("192.168.1.1/32"); // pass
-ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // pass
-ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1/64"); // pass
+ipv4Range.parse("192.168.1.10"); // pass
+ipv4Range.parse("192.168.1.20"); // pass
+ipv4Range.parse("192.168.2.10"); // fail
-ipRange.parse("256.1.1.1/32"); // fail
-ipRange.parse("192.168.1.1/33"); // fail
-ipRange.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003/128"); // fail
-ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/129"); // fail
-```
-
-You can additionally set the IP `version`.
-
-```ts
-const ipv4Range = z.string().ipRange({ version: "v4" });
-ipv4Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // fail
-ipv4Range.parse("192.168.1.1/128"); // fail
+const ipv6Range = z.string().ipRange({ cidr: "2001:db8::/32", version: "v6" });
-const ipv6Range = z.string().ipRange({ version: "v6" });
-ipv6Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/32"); // fail
-ipv6Range.parse("192.168.1.1/32"); // fail
+ipv6Range.parse("2001:db8::1"); // pass
+ipv6Range.parse("2001:db9::1"); // fail
```
## Numbers
diff --git a/deno/lib/README.md b/deno/lib/README.md
index 1a2da4f26..0c5b42834 100644
--- a/deno/lib/README.md
+++ b/deno/lib/README.md
@@ -811,7 +811,6 @@ 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().ipRange({ message: "Invalid IP address range" });
```
### Datetimes
@@ -919,31 +918,19 @@ ipv6.parse("192.168.1.1"); // fail
### IP addresses range
-The `z.string().ipRange()` method by default validate IPv4 and IPv6.
+The `z.string().ipRange()` method checks if a given IP address falls within a specified CIDR range.
```ts
-const ipRange = z.string().ipRange();
+const ipv4Range = z.string().ipRange({ cidr: "192.168.1.0/24", version: "v4" });
-ipRange.parse("192.168.1.1/32"); // pass
-ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // pass
-ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1/64"); // pass
+ipv4Range.parse("192.168.1.10"); // pass
+ipv4Range.parse("192.168.1.20"); // pass
+ipv4Range.parse("192.168.2.10"); // fail
-ipRange.parse("256.1.1.1/32"); // fail
-ipRange.parse("192.168.1.1/33"); // fail
-ipRange.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003/128"); // fail
-ipRange.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/129"); // fail
-```
-
-You can additionally set the IP `version`.
-
-```ts
-const ipv4Range = z.string().ipRange({ version: "v4" });
-ipv4Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/128"); // fail
-ipv4Range.parse("192.168.1.1/128"); // fail
+const ipv6Range = z.string().ipRange({ cidr: "2001:db8::/32", version: "v6" });
-const ipv6Range = z.string().ipRange({ version: "v6" });
-ipv6Range.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003/32"); // fail
-ipv6Range.parse("192.168.1.1/32"); // fail
+ipv6Range.parse("2001:db8::1"); // pass
+ipv6Range.parse("2001:db9::1"); // fail
```
## Numbers
diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index 6e0b2a05a..c1dd0c9d9 100644
--- a/deno/lib/__tests__/string.test.ts
+++ b/deno/lib/__tests__/string.test.ts
@@ -436,16 +436,6 @@ test("checks getters", () => {
expect(z.string().ip().isIPRange).toEqual(false);
expect(z.string().ip().isULID).toEqual(false);
- expect(z.string().ipRange().isEmail).toEqual(false);
- expect(z.string().ipRange().isURL).toEqual(false);
- expect(z.string().ipRange().isCUID).toEqual(false);
- expect(z.string().ipRange().isCUID2).toEqual(false);
- expect(z.string().ipRange().isUUID).toEqual(false);
- expect(z.string().ipRange().isNANOID).toEqual(false);
- expect(z.string().ipRange().isIP).toEqual(false);
- expect(z.string().ipRange().isIPRange).toEqual(true);
- expect(z.string().ipRange().isULID).toEqual(false);
-
expect(z.string().ulid().isEmail).toEqual(false);
expect(z.string().ulid().isURL).toEqual(false);
expect(z.string().ulid().isCUID).toEqual(false);
@@ -789,56 +779,83 @@ test("IP validation", () => {
});
test("IP Range validation", () => {
- const ipRange = z.string().ipRange();
- expect(ipRange.safeParse("122.122.122.122/0").success).toBe(true);
- expect(ipRange.safeParse("122.122.122.122/32").success).toBe(true);
- expect(ipRange.safeParse("122.122.122.122/-1").success).toBe(false);
- expect(ipRange.safeParse("122.122.122.122/33").success).toBe(false);
+ const ipv4Range = z
+ .string()
+ .ipRange({ cidr: "192.168.1.0/33", version: "v4" });
+ expect(() => ipv4Range.parse("192.168.1.1")).toThrow();
- const ipv4Range = z.string().ipRange({ version: "v4" });
- expect(() =>
- ipv4Range.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990/128")
- ).toThrow();
+ const ipv6Range = z
+ .string()
+ .ipRange({ cidr: "2001:db8::/129", version: "v6" });
+ expect(() => ipv6Range.parse("2001:db9::1")).toThrow();
+
+ const validIPv4Ranges = [
+ { ip: "192.168.1.10", cidr: "192.168.1.0/24" },
+ { ip: "192.168.1.20", cidr: "192.168.1.0/24" },
+ { ip: "172.16.0.5", cidr: "172.16.0.0/12" },
+ { ip: "10.0.0.1", cidr: "10.0.0.0/8" },
+ { ip: "2001:db8::1", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:1234:5678:abcd:ef01:2345:6789", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:85a3::8a2e:370:7334", cidr: "2001:db8::/32" },
+ { ip: "0.0.0.0", cidr: "0.0.0.0/0" },
+ { ip: "37.85.236.115", cidr: "37.85.236.0/24" },
+ { ip: "114.71.82.94", cidr: "114.71.82.0/24" },
+ ];
- const ipv6Range = z.string().ipRange({ version: "v6" });
- expect(() => ipv6Range.parse("254.164.77.1/32")).toThrow();
-
- const validIPRanges = [
- "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/0",
- "9d4:c956:420f:5788:4339:9b3b:2418:75c3/128",
- "474f:4c83::4e40:a47:ff95:0cda/16",
- "d329:0:25b4:db47:a9d1:0:4926:0000/64",
- "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/8",
- "114.71.82.94/0",
- "0.0.0.0/0",
- "37.85.236.115/32",
+ const invalidIPv4Ranges = [
+ { ip: "192.168.2.10", cidr: "192.168.1.0/24" },
+ { ip: "192.168.2.5", cidr: "192.168.1.0/24" },
+ { ip: "10.1.1.1", cidr: "11.0.0.0/8" },
+ { ip: "37.85.237.115", cidr: "37.85.236.0/24" },
+ { ip: "114.71.83.94", cidr: "114.71.82.0/24" },
+ { ip: "172.32.0.1", cidr: "172.16.0.0/12" },
];
- const invalidIPRanges = [
- "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/129",
- "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af/128",
- "474f:4c83::4e40:a47:ff95:0cda/129",
- "8f69::c757:395e:976e::3441/64",
- "54cb::473f:d516:0.255.256.22/64",
- "54cb::473f:d516:192.168.1/64",
- "474f:4c83::4e40:a47:ff95:0cda/129",
- "d329:0:25b4:db47:a9d1:0:4926:0000/129",
- "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/129",
- "256.0.4.4/16",
- "-1.0.555.4/32",
- "0.0.0.0.0/0",
- "1.1.1/16",
- "114.71.82.94/128",
- "0.0.0.0/33",
+ expect(
+ validIPv4Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v4" });
+
+ return ipRangeSchema.safeParse(ip).success;
+ })
+ ).toBe(true);
+ expect(
+ invalidIPv4Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v4" });
+
+ return ipRangeSchema.safeParse(ip).success === false;
+ })
+ ).toBe(true);
+
+ const validIPv6Ranges = [
+ { ip: "2001:db8::1", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:1234:5678:abcd:ef01:2345:6789", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:85a3::8a2e:370:7334", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:abcd:ef01:2345:6789:abcd:ef01", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:0:1:0:0:0:1", cidr: "2001:db8:0:1::/64" },
+ { ip: "fe80::1", cidr: "fe80::/10" },
+ { ip: "2001:4888:50:ff00:500:d::", cidr: "2001:4888:50:ff00::/64" },
];
- // no parameters check IPv4 or IPv6
- const ipRangeSchema = z.string().ipRange();
+
+ const invalidIPv6Ranges = [
+ { ip: "::1", cidr: "::/128" },
+ { ip: "2001:db9::1", cidr: "2001:db8::/32" },
+ { ip: "fe80::1", cidr: "2001:db8::/32" },
+ { ip: "ff00::1", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:abcd:1234::1", cidr: "2001:db8:abcd:5678::/64" },
+ ];
+
expect(
- validIPRanges.every((ipRange) => ipRangeSchema.safeParse(ipRange).success)
+ validIPv6Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" });
+
+ return ipRangeSchema.safeParse(ip).success;
+ })
).toBe(true);
expect(
- invalidIPRanges.every(
- (ipRange) => ipRangeSchema.safeParse(ipRange).success === false
- )
+ invalidIPv6Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" });
+
+ return ipRangeSchema.safeParse(ip).success === false;
+ })
).toBe(true);
});
diff --git a/deno/lib/types.ts b/deno/lib/types.ts
index 94b78ec68..841ea6231 100644
--- a/deno/lib/types.ts
+++ b/deno/lib/types.ts
@@ -565,7 +565,7 @@ export type ZodStringCheck =
}
| { kind: "duration"; message?: string }
| { kind: "ip"; version?: IpVersion; message?: string }
- | { kind: "ipRange"; version?: IpVersion; message?: string }
+ | { kind: "ipRange"; cidr: string; version: IpVersion; message?: string }
| { kind: "base64"; message?: string };
export interface ZodStringDef extends ZodTypeDef {
@@ -609,11 +609,9 @@ 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 = /^(3[0-2]|[12]?[0-9])$/;
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 = /^(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 =
@@ -672,34 +670,55 @@ function isValidIP(ip: string, version?: IpVersion) {
return false;
}
-function isValidIPRange(ip: string, version?: IpVersion) {
- const [ipAddress, mask] = ip.split("/");
- if (ipAddress === undefined || mask === undefined) {
- return false;
- }
- if (
- !version &&
- ((ipv4Regex.test(ipAddress) && ipv4CidrRegex.test(mask)) ||
- (ipv6Regex.test(ipAddress) && ipv6CidrRegex.test(mask)))
- ) {
- return true;
- }
- if (
- version === "v4" &&
- ipv4Regex.test(ipAddress) &&
- ipv4CidrRegex.test(mask)
- ) {
- return true;
+const ipToBinary = (ip: string, version: IpVersion): string => {
+ if (version === "v4") {
+ return ip
+ .split(".")
+ .map((octet) => parseInt(octet, 10).toString(2).padStart(8, "0"))
+ .join("");
+ } else if (version === "v6") {
+ const segments = ip.split(":");
+ const fullSegments: string[] = [];
+ let zeroFillCount = 0;
+
+ for (const segment of segments) {
+ if (segment === "") {
+ zeroFillCount = 8 - (segments.length - 1);
+ continue;
+ }
+ fullSegments.push(segment.padStart(4, "0"));
+ }
+
+ for (let i = 0; i < zeroFillCount; i++) {
+ fullSegments.splice(segments.indexOf("") + i, 0, "0000");
+ }
+
+ return fullSegments
+ .map((segment) => parseInt(segment, 16).toString(2).padStart(16, "0"))
+ .join("");
+ } else {
+ throw new Error("Invalid IP version");
}
+};
+
+function isValidIPRange(ip: string, cidr: string, version: IpVersion) {
+ const [rangeIp, prefixLengthStr] = cidr.split("/");
+ const prefixLength = parseInt(prefixLengthStr, 10);
+
if (
- version === "v6" &&
- ipv6Regex.test(ipAddress) &&
- ipv6CidrRegex.test(mask)
+ (version === "v4" && (prefixLength < 0 || prefixLength > 32)) ||
+ (version === "v6" && (prefixLength < 0 || prefixLength > 128))
) {
- return true;
+ throw new Error("Invalid prefix length");
}
- return false;
+ const ipBinary = ipToBinary(ip, version);
+ const rangeBinary = ipToBinary(rangeIp, version);
+
+ return (
+ ipBinary.substring(0, prefixLength) ===
+ rangeBinary.substring(0, prefixLength)
+ );
}
export class ZodString extends ZodType {
@@ -965,7 +984,7 @@ export class ZodString extends ZodType {
status.dirty();
}
} else if (check.kind === "ipRange") {
- if (!isValidIPRange(input.data, check.version)) {
+ if (!isValidIPRange(input.data, check.cidr, check.version)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "ipRange",
@@ -1047,9 +1066,14 @@ export class ZodString extends ZodType {
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
}
- ipRange(options?: string | { version?: IpVersion; message?: string }) {
+ ipRange(
+ args: { cidr: string; version: IpVersion },
+ options?: string | { message?: string }
+ ) {
return this._addCheck({
kind: "ipRange",
+ cidr: args.cidr,
+ version: args.version,
...errorUtil.errToObj(options),
});
}
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index 5d59969de..dec5c6bf7 100644
--- a/src/__tests__/string.test.ts
+++ b/src/__tests__/string.test.ts
@@ -435,16 +435,6 @@ test("checks getters", () => {
expect(z.string().ip().isIPRange).toEqual(false);
expect(z.string().ip().isULID).toEqual(false);
- expect(z.string().ipRange().isEmail).toEqual(false);
- expect(z.string().ipRange().isURL).toEqual(false);
- expect(z.string().ipRange().isCUID).toEqual(false);
- expect(z.string().ipRange().isCUID2).toEqual(false);
- expect(z.string().ipRange().isUUID).toEqual(false);
- expect(z.string().ipRange().isNANOID).toEqual(false);
- expect(z.string().ipRange().isIP).toEqual(false);
- expect(z.string().ipRange().isIPRange).toEqual(true);
- expect(z.string().ipRange().isULID).toEqual(false);
-
expect(z.string().ulid().isEmail).toEqual(false);
expect(z.string().ulid().isURL).toEqual(false);
expect(z.string().ulid().isCUID).toEqual(false);
@@ -788,56 +778,83 @@ test("IP validation", () => {
});
test("IP Range validation", () => {
- const ipRange = z.string().ipRange();
- expect(ipRange.safeParse("122.122.122.122/0").success).toBe(true);
- expect(ipRange.safeParse("122.122.122.122/32").success).toBe(true);
- expect(ipRange.safeParse("122.122.122.122/-1").success).toBe(false);
- expect(ipRange.safeParse("122.122.122.122/33").success).toBe(false);
+ const ipv4Range = z
+ .string()
+ .ipRange({ cidr: "192.168.1.0/33", version: "v4" });
+ expect(() => ipv4Range.parse("192.168.1.1")).toThrow();
- const ipv4Range = z.string().ipRange({ version: "v4" });
- expect(() =>
- ipv4Range.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990/128")
- ).toThrow();
+ const ipv6Range = z
+ .string()
+ .ipRange({ cidr: "2001:db8::/129", version: "v6" });
+ expect(() => ipv6Range.parse("2001:db9::1")).toThrow();
+
+ const validIPv4Ranges = [
+ { ip: "192.168.1.10", cidr: "192.168.1.0/24" },
+ { ip: "192.168.1.20", cidr: "192.168.1.0/24" },
+ { ip: "172.16.0.5", cidr: "172.16.0.0/12" },
+ { ip: "10.0.0.1", cidr: "10.0.0.0/8" },
+ { ip: "2001:db8::1", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:1234:5678:abcd:ef01:2345:6789", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:85a3::8a2e:370:7334", cidr: "2001:db8::/32" },
+ { ip: "0.0.0.0", cidr: "0.0.0.0/0" },
+ { ip: "37.85.236.115", cidr: "37.85.236.0/24" },
+ { ip: "114.71.82.94", cidr: "114.71.82.0/24" },
+ ];
- const ipv6Range = z.string().ipRange({ version: "v6" });
- expect(() => ipv6Range.parse("254.164.77.1/32")).toThrow();
-
- const validIPRanges = [
- "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/0",
- "9d4:c956:420f:5788:4339:9b3b:2418:75c3/128",
- "474f:4c83::4e40:a47:ff95:0cda/16",
- "d329:0:25b4:db47:a9d1:0:4926:0000/64",
- "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/8",
- "114.71.82.94/0",
- "0.0.0.0/0",
- "37.85.236.115/32",
+ const invalidIPv4Ranges = [
+ { ip: "192.168.2.10", cidr: "192.168.1.0/24" },
+ { ip: "192.168.2.5", cidr: "192.168.1.0/24" },
+ { ip: "10.1.1.1", cidr: "11.0.0.0/8" },
+ { ip: "37.85.237.115", cidr: "37.85.236.0/24" },
+ { ip: "114.71.83.94", cidr: "114.71.82.0/24" },
+ { ip: "172.32.0.1", cidr: "172.16.0.0/12" },
];
- const invalidIPRanges = [
- "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c/129",
- "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af/128",
- "474f:4c83::4e40:a47:ff95:0cda/129",
- "8f69::c757:395e:976e::3441/64",
- "54cb::473f:d516:0.255.256.22/64",
- "54cb::473f:d516:192.168.1/64",
- "474f:4c83::4e40:a47:ff95:0cda/129",
- "d329:0:25b4:db47:a9d1:0:4926:0000/129",
- "e48:10fb:1499:3e28:e4b6:dea5:4692:912c/129",
- "256.0.4.4/16",
- "-1.0.555.4/32",
- "0.0.0.0.0/0",
- "1.1.1/16",
- "114.71.82.94/128",
- "0.0.0.0/33",
+ expect(
+ validIPv4Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v4" });
+
+ return ipRangeSchema.safeParse(ip).success;
+ })
+ ).toBe(true);
+ expect(
+ invalidIPv4Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v4" });
+
+ return ipRangeSchema.safeParse(ip).success === false;
+ })
+ ).toBe(true);
+
+ const validIPv6Ranges = [
+ { ip: "2001:db8::1", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:1234:5678:abcd:ef01:2345:6789", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:85a3::8a2e:370:7334", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:abcd:ef01:2345:6789:abcd:ef01", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:0:1:0:0:0:1", cidr: "2001:db8:0:1::/64" },
+ { ip: "fe80::1", cidr: "fe80::/10" },
+ { ip: "2001:4888:50:ff00:500:d::", cidr: "2001:4888:50:ff00::/64" },
];
- // no parameters check IPv4 or IPv6
- const ipRangeSchema = z.string().ipRange();
+
+ const invalidIPv6Ranges = [
+ { ip: "::1", cidr: "::/128" },
+ { ip: "2001:db9::1", cidr: "2001:db8::/32" },
+ { ip: "fe80::1", cidr: "2001:db8::/32" },
+ { ip: "ff00::1", cidr: "2001:db8::/32" },
+ { ip: "2001:db8:abcd:1234::1", cidr: "2001:db8:abcd:5678::/64" },
+ ];
+
expect(
- validIPRanges.every((ipRange) => ipRangeSchema.safeParse(ipRange).success)
+ validIPv6Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" });
+
+ return ipRangeSchema.safeParse(ip).success;
+ })
).toBe(true);
expect(
- invalidIPRanges.every(
- (ipRange) => ipRangeSchema.safeParse(ipRange).success === false
- )
+ invalidIPv6Ranges.every(({ ip, cidr }) => {
+ const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" });
+
+ return ipRangeSchema.safeParse(ip).success === false;
+ })
).toBe(true);
});
diff --git a/src/types.ts b/src/types.ts
index 50676e1c3..00c2882a7 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -565,7 +565,7 @@ export type ZodStringCheck =
}
| { kind: "duration"; message?: string }
| { kind: "ip"; version?: IpVersion; message?: string }
- | { kind: "ipRange"; version?: IpVersion; message?: string }
+ | { kind: "ipRange"; cidr: string; version: IpVersion; message?: string }
| { kind: "base64"; message?: string };
export interface ZodStringDef extends ZodTypeDef {
@@ -609,11 +609,9 @@ 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 = /^(3[0-2]|[12]?[0-9])$/;
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 = /^(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 =
@@ -672,34 +670,55 @@ function isValidIP(ip: string, version?: IpVersion) {
return false;
}
-function isValidIPRange(ip: string, version?: IpVersion) {
- const [ipAddress, mask] = ip.split("/");
- if (ipAddress === undefined || mask === undefined) {
- return false;
- }
- if (
- !version &&
- ((ipv4Regex.test(ipAddress) && ipv4CidrRegex.test(mask)) ||
- (ipv6Regex.test(ipAddress) && ipv6CidrRegex.test(mask)))
- ) {
- return true;
- }
- if (
- version === "v4" &&
- ipv4Regex.test(ipAddress) &&
- ipv4CidrRegex.test(mask)
- ) {
- return true;
+const ipToBinary = (ip: string, version: IpVersion): string => {
+ if (version === "v4") {
+ return ip
+ .split(".")
+ .map((octet) => parseInt(octet, 10).toString(2).padStart(8, "0"))
+ .join("");
+ } else if (version === "v6") {
+ const segments = ip.split(":");
+ const fullSegments: string[] = [];
+ let zeroFillCount = 0;
+
+ for (const segment of segments) {
+ if (segment === "") {
+ zeroFillCount = 8 - (segments.length - 1);
+ continue;
+ }
+ fullSegments.push(segment.padStart(4, "0"));
+ }
+
+ for (let i = 0; i < zeroFillCount; i++) {
+ fullSegments.splice(segments.indexOf("") + i, 0, "0000");
+ }
+
+ return fullSegments
+ .map((segment) => parseInt(segment, 16).toString(2).padStart(16, "0"))
+ .join("");
+ } else {
+ throw new Error("Invalid IP version");
}
+};
+
+function isValidIPRange(ip: string, cidr: string, version: IpVersion) {
+ const [rangeIp, prefixLengthStr] = cidr.split("/");
+ const prefixLength = parseInt(prefixLengthStr, 10);
+
if (
- version === "v6" &&
- ipv6Regex.test(ipAddress) &&
- ipv6CidrRegex.test(mask)
+ (version === "v4" && (prefixLength < 0 || prefixLength > 32)) ||
+ (version === "v6" && (prefixLength < 0 || prefixLength > 128))
) {
- return true;
+ throw new Error("Invalid prefix length");
}
- return false;
+ const ipBinary = ipToBinary(ip, version);
+ const rangeBinary = ipToBinary(rangeIp, version);
+
+ return (
+ ipBinary.substring(0, prefixLength) ===
+ rangeBinary.substring(0, prefixLength)
+ );
}
export class ZodString extends ZodType {
@@ -965,7 +984,7 @@ export class ZodString extends ZodType {
status.dirty();
}
} else if (check.kind === "ipRange") {
- if (!isValidIPRange(input.data, check.version)) {
+ if (!isValidIPRange(input.data, check.cidr, check.version)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "ipRange",
@@ -1047,9 +1066,14 @@ export class ZodString extends ZodType {
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
}
- ipRange(options?: string | { version?: IpVersion; message?: string }) {
+ ipRange(
+ args: { cidr: string; version: IpVersion },
+ options?: string | { message?: string }
+ ) {
return this._addCheck({
kind: "ipRange",
+ cidr: args.cidr,
+ version: args.version,
...errorUtil.errToObj(options),
});
}