Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
wataryooou committed Oct 27, 2024
1 parent db35eaa commit 45428a3
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 204 deletions.
29 changes: 8 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 8 additions & 21 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
123 changes: 70 additions & 53 deletions deno/lib/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
80 changes: 52 additions & 28 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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<string, ZodStringDef, string> {
Expand Down Expand Up @@ -965,7 +984,7 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
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",
Expand Down Expand Up @@ -1047,9 +1066,14 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
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),
});
}
Expand Down
Loading

0 comments on commit 45428a3

Please sign in to comment.