diff --git a/README.md b/README.md index 83af7ed6b..aa6514ce4 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ - [Dates](#dates) - [Times](#times) - [IP addresses](#ip-addresses) + - [IP addresses range](#ip-addresses-range) - [IP ranges](#ip-ranges-cidr) - [Numbers](#numbers) - [BigInts](#bigints) @@ -933,6 +934,23 @@ const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail ``` +### IP addresses range + +The `z.string().ipRange()` method checks if a given IP address falls within a specified CIDR range. + +```ts +const ipv4Range = z.string().ipRange({ cidr: "192.168.1.0/24", version: "v4" }); + +ipv4Range.parse("192.168.1.10"); // pass +ipv4Range.parse("192.168.1.20"); // pass +ipv4Range.parse("192.168.2.10"); // fail + +const ipv6Range = z.string().ipRange({ cidr: "2001:db8::/32", version: "v6" }); + +ipv6Range.parse("2001:db8::1"); // pass +ipv6Range.parse("2001:db9::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. diff --git a/deno/lib/README.md b/deno/lib/README.md index 83af7ed6b..aa6514ce4 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -75,6 +75,7 @@ - [Dates](#dates) - [Times](#times) - [IP addresses](#ip-addresses) + - [IP addresses range](#ip-addresses-range) - [IP ranges](#ip-ranges-cidr) - [Numbers](#numbers) - [BigInts](#bigints) @@ -933,6 +934,23 @@ const ipv6 = z.string().ip({ version: "v6" }); ipv6.parse("192.168.1.1"); // fail ``` +### IP addresses range + +The `z.string().ipRange()` method checks if a given IP address falls within a specified CIDR range. + +```ts +const ipv4Range = z.string().ipRange({ cidr: "192.168.1.0/24", version: "v4" }); + +ipv4Range.parse("192.168.1.10"); // pass +ipv4Range.parse("192.168.1.20"); // pass +ipv4Range.parse("192.168.2.10"); // fail + +const ipv6Range = z.string().ipRange({ cidr: "2001:db8::/32", version: "v6" }); + +ipv6Range.parse("2001:db8::1"); // pass +ipv6Range.parse("2001:db9::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. diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index 38af7bf8c..883804b50 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -103,6 +103,7 @@ export type StringValidation = | "time" | "duration" | "ip" + | "ipRange" | "cidr" | "base64" | "jwt" diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index bb967bafc..3c666b679 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -463,6 +463,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().isCIDR).toEqual(false); expect(z.string().email().isULID).toEqual(false); @@ -473,6 +474,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().isCIDR).toEqual(false); expect(z.string().url().isULID).toEqual(false); @@ -483,6 +485,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().isCIDR).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); @@ -493,6 +496,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().isCIDR).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); @@ -503,6 +507,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().isCIDR).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); @@ -513,6 +518,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().isCIDR).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); @@ -523,6 +529,7 @@ 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().isCIDR).toEqual(false); expect(z.string().ip().isULID).toEqual(false); @@ -543,6 +550,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().isCIDR).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); }); @@ -878,6 +886,88 @@ test("IP validation", () => { ).toBe(true); }); +test("IP Range validation", () => { + const ipv4Range = z + .string() + .ipRange({ cidr: "192.168.1.0/33", version: "v4" }); + expect(() => ipv4Range.parse("192.168.1.1")).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 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" }, + ]; + + 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" }, + ]; + + 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( + validIPv6Ranges.every(({ ip, cidr }) => { + const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" }); + + return ipRangeSchema.safeParse(ip).success; + }) + ).toBe(true); + expect( + invalidIPv6Ranges.every(({ ip, cidr }) => { + const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" }); + + return ipRangeSchema.safeParse(ip).success === false; + }) + ); +}); + test("CIDR validation", () => { const ipv4Cidr = z.string().cidr({ version: "v4" }); expect(() => ipv4Cidr.parse("2001:0db8:85a3::8a2e:0370:7334/64")).toThrow(); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index cd09d4b15..7638b4093 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -623,6 +623,7 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } + | { kind: "ipRange"; cidr: string; version: IpVersion; message?: string } | { kind: "cidr"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string } | { kind: "base64url"; message?: string }; @@ -740,6 +741,68 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +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 === "v4" && (prefixLength < 0 || prefixLength > 32)) || + (version === "v6" && (prefixLength < 0 || prefixLength > 128)) + ) { + throw new Error("Invalid prefix length"); + } + + const ipBinary = ipToBinary(ip, version); + const rangeBinary = ipToBinary(rangeIp, version); + + return ( + ipBinary.substring(0, prefixLength) === + rangeBinary.substring(0, prefixLength) + ); +} + +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; +} + function isValidJWT(jwt: string, alg?: string): boolean { if (!jwtRegex.test(jwt)) return false; try { @@ -759,17 +822,6 @@ function isValidJWT(jwt: string, alg?: string): boolean { } } -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) { @@ -1032,11 +1084,11 @@ export class ZodString extends ZodType { }); status.dirty(); } - } else if (check.kind === "jwt") { - if (!isValidJWT(input.data, check.alg)) { + } else if (check.kind === "ipRange") { + if (!isValidIPRange(input.data, check.cidr, check.version)) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { - validation: "jwt", + validation: "ipRange", code: ZodIssueCode.invalid_string, message: check.message, }); @@ -1052,6 +1104,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "base64") { if (!base64Regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1146,6 +1208,18 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } + ipRange( + args: { cidr: string; version: IpVersion }, + options?: string | { message?: string } + ) { + return this._addCheck({ + kind: "ipRange", + cidr: args.cidr, + version: args.version, + ...errorUtil.errToObj(options), + }); + } + cidr(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); } @@ -1342,6 +1416,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 isCIDR() { return !!this._def.checks.find((ch) => ch.kind === "cidr"); } diff --git a/src/ZodError.ts b/src/ZodError.ts index e10e1622d..13ca4d63b 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -103,6 +103,7 @@ export type StringValidation = | "time" | "duration" | "ip" + | "ipRange" | "cidr" | "base64" | "jwt" diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index bc603a933..946dc1ba8 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -462,6 +462,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().isCIDR).toEqual(false); expect(z.string().email().isULID).toEqual(false); @@ -472,6 +473,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().isCIDR).toEqual(false); expect(z.string().url().isULID).toEqual(false); @@ -482,6 +484,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().isCIDR).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); @@ -492,6 +495,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().isCIDR).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); @@ -502,6 +506,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().isCIDR).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); @@ -512,6 +517,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().isCIDR).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); @@ -522,6 +528,7 @@ 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().isCIDR).toEqual(false); expect(z.string().ip().isULID).toEqual(false); @@ -542,6 +549,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().isCIDR).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); }); @@ -877,6 +885,88 @@ test("IP validation", () => { ).toBe(true); }); +test("IP Range validation", () => { + const ipv4Range = z + .string() + .ipRange({ cidr: "192.168.1.0/33", version: "v4" }); + expect(() => ipv4Range.parse("192.168.1.1")).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 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" }, + ]; + + 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" }, + ]; + + 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( + validIPv6Ranges.every(({ ip, cidr }) => { + const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" }); + + return ipRangeSchema.safeParse(ip).success; + }) + ).toBe(true); + expect( + invalidIPv6Ranges.every(({ ip, cidr }) => { + const ipRangeSchema = z.string().ipRange({ cidr, version: "v6" }); + + return ipRangeSchema.safeParse(ip).success === false; + }) + ); +}); + test("CIDR validation", () => { const ipv4Cidr = z.string().cidr({ version: "v4" }); expect(() => ipv4Cidr.parse("2001:0db8:85a3::8a2e:0370:7334/64")).toThrow(); diff --git a/src/types.ts b/src/types.ts index 98281ff2f..0e05c973e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -623,6 +623,7 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } + | { kind: "ipRange"; cidr: string; version: IpVersion; message?: string } | { kind: "cidr"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string } | { kind: "base64url"; message?: string }; @@ -740,6 +741,68 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +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 === "v4" && (prefixLength < 0 || prefixLength > 32)) || + (version === "v6" && (prefixLength < 0 || prefixLength > 128)) + ) { + throw new Error("Invalid prefix length"); + } + + const ipBinary = ipToBinary(ip, version); + const rangeBinary = ipToBinary(rangeIp, version); + + return ( + ipBinary.substring(0, prefixLength) === + rangeBinary.substring(0, prefixLength) + ); +} + +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; +} + function isValidJWT(jwt: string, alg?: string): boolean { if (!jwtRegex.test(jwt)) return false; try { @@ -759,17 +822,6 @@ function isValidJWT(jwt: string, alg?: string): boolean { } } -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) { @@ -1032,11 +1084,11 @@ export class ZodString extends ZodType { }); status.dirty(); } - } else if (check.kind === "jwt") { - if (!isValidJWT(input.data, check.alg)) { + } else if (check.kind === "ipRange") { + if (!isValidIPRange(input.data, check.cidr, check.version)) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { - validation: "jwt", + validation: "ipRange", code: ZodIssueCode.invalid_string, message: check.message, }); @@ -1052,6 +1104,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "base64") { if (!base64Regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1146,6 +1208,18 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } + ipRange( + args: { cidr: string; version: IpVersion }, + options?: string | { message?: string } + ) { + return this._addCheck({ + kind: "ipRange", + cidr: args.cidr, + version: args.version, + ...errorUtil.errToObj(options), + }); + } + cidr(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); } @@ -1342,6 +1416,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 isCIDR() { return !!this._def.checks.find((ch) => ch.kind === "cidr"); }