Skip to content

Commit

Permalink
Add support for base64url strings
Browse files Browse the repository at this point in the history
Fixes #3711

Signed-off-by: Marvin A. Ruder <signed@mruder.dev>
  • Loading branch information
marvinruder committed Aug 16, 2024
1 parent 1b38419 commit 070c899
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type StringValidation =
| "duration"
| "ip"
| "base64"
| "base64url"
| { includes: string; position?: number }
| { startsWith: string }
| { endsWith: string };
Expand Down
51 changes: 50 additions & 1 deletion src/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ test("base64 validations", () => {
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz"
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"ISIkJSMmJyonKCk=", // "!\"#$%&'()*"
"", // Empty string is technically a valid base64
"", // Empty string is technically valid base64
"w7/Dv8O+w74K", // ÿÿþþ
];

for (const str of validBase64Strings) {
Expand All @@ -184,12 +185,14 @@ test("base64 validations", () => {

const invalidBase64Strings = [
"12345", // Not padded correctly, not a multiple of 4 characters
"12345===", // Not padded correctly
"SGVsbG8gV29ybGQ", // Missing padding
"VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // Missing padding
"!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!'
"?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?'
".MTIzND2Nzg5MC4=", // Invalid character '.'
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // Missing padding
"w7_Dv8O-w74K", // Has - and _ characters (is base64url)
];

for (const str of invalidBase64Strings) {
Expand All @@ -199,6 +202,52 @@ test("base64 validations", () => {
}
});

test("base64url validations", () => {
const validBase64urlStrings = [
"SGVsbG8gV29ybGQ", // "Hello World"
"SGVsbG8gV29ybGQ=", // "Hello World" with padding
"VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // "This is an encoded string"
"VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string" with padding
"TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms", // "Many hands make light work"
"TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work" with padding
"UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success"
"QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg", // "Base64 encoding is fun"
"QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun" with padding
"MTIzNDU2Nzg5MA", // "1234567890"
"MTIzNDU2Nzg5MA==", // "1234567890" with padding
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo", // "abcdefghijklmnopqrstuvwxyz"
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz with padding"
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" with padding
"ISIkJSMmJyonKCk", // "!\"#$%&'()*"
"ISIkJSMmJyonKCk=", // "!\"#$%&'()*" with padding
"", // Empty string is technically valid base64url
"w7_Dv8O-w74K", // ÿÿþþ
"123456",
];

for (const str of validBase64urlStrings) {
expect(str + z.string().base64url().safeParse(str).success).toBe(
str + "true"
);
}

const invalidBase64urlStrings = [
"w7/Dv8O+w74K", // Has + and / characters (is base64)
"12345", // Invalid length (not a multiple of 4 characters when adding allowed number of padding characters)
"12345===", // Not padded correctly
"!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!'
"?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?'
".MTIzND2Nzg5MC4=", // Invalid character '.'
];

for (const str of invalidBase64urlStrings) {
expect(str + z.string().base64url().safeParse(str).success).toBe(
str + "false"
);
}
});

test("url validations", () => {
const url = z.string().url();
url.parse("http://google.com");
Expand Down
23 changes: 22 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,8 @@ export type ZodStringCheck =
}
| { kind: "duration"; message?: string }
| { kind: "ip"; version?: IpVersion; message?: string }
| { kind: "base64"; message?: string };
| { kind: "base64"; message?: string }
| { kind: "base64url"; message?: string };

export interface ZodStringDef extends ZodTypeDef {
checks: ZodStringCheck[];
Expand Down Expand Up @@ -616,6 +617,10 @@ const ipv6Regex =
const base64Regex =
/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;

// https://base64.guru/standards/base64url
const base64urlRegex =
/^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/;

// simple
// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`;
// no leap year validation
Expand Down Expand Up @@ -941,6 +946,16 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
});
status.dirty();
}
} else if (check.kind === "base64url") {
if (!base64urlRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "base64url",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else {
util.assertNever(check);
}
Expand Down Expand Up @@ -999,6 +1014,9 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
base64(message?: errorUtil.ErrMessage) {
return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) });
}
base64url(message?: errorUtil.ErrMessage) {
return this._addCheck({ kind: "base64url", ...errorUtil.errToObj(message) });
}

ip(options?: string | { version?: "v4" | "v6"; message?: string }) {
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
Expand Down Expand Up @@ -1200,6 +1218,9 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
get isBase64() {
return !!this._def.checks.find((ch) => ch.kind === "base64");
}
get isBase64url() {
return !!this._def.checks.find((ch) => ch.kind === "base64url");
}

get minLength() {
let min: number | null = null;
Expand Down

0 comments on commit 070c899

Please sign in to comment.