Skip to content

Commit 96f6f5d

Browse files
author
Stefan Terdell
committed
Add base64 string validation
1 parent 0e89027 commit 96f6f5d

File tree

8 files changed

+114
-2
lines changed

8 files changed

+114
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@ z.string().startsWith(string);
747747
z.string().endsWith(string);
748748
z.string().datetime(); // ISO 8601; default is without UTC offset, see below for options
749749
z.string().ip(); // defaults to IPv4 and IPv6, see below for options
750+
z.string().base64();
750751

751752
// transformations
752753
z.string().trim(); // trim whitespace

deno/lib/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@ z.string().startsWith(string);
747747
z.string().endsWith(string);
748748
z.string().datetime(); // ISO 8601; default is without UTC offset, see below for options
749749
z.string().ip(); // defaults to IPv4 and IPv6, see below for options
750+
z.string().base64();
750751

751752
// transformations
752753
z.string().trim(); // trim whitespace

deno/lib/ZodError.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export type StringValidation =
9999
| "ulid"
100100
| "datetime"
101101
| "ip"
102+
| "base64"
102103
| { includes: string; position?: number }
103104
| { startsWith: string }
104105
| { endsWith: string };

deno/lib/__tests__/string.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,39 @@ test("email validations", () => {
162162
).toBe(true);
163163
});
164164

165+
test("base64 validations", () => {
166+
const validBase64Strings = [
167+
"SGVsbG8gV29ybGQ=", // "Hello World"
168+
"VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string"
169+
"TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work"
170+
"UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success"
171+
"QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun"
172+
"MTIzNDU2Nzg5MA==", // "1234567890"
173+
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz"
174+
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
175+
"ISIkJSMmJyonKCk=", // "!\"#$%&'()*"
176+
"" // Empty string is technically a valid base64
177+
];
178+
179+
for (const str of validBase64Strings) {
180+
expect(str + z.string().base64().safeParse(str).success).toBe(str + "true");
181+
}
182+
183+
const invalidBase64Strings = [
184+
"12345", // Not padded correctly, not a multiple of 4 characters
185+
"SGVsbG8gV29ybGQ", // Missing padding
186+
"VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // Missing padding
187+
"!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!'
188+
"?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?'
189+
".MTIzND2Nzg5MC4=", // Invalid character '.'
190+
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // Missing padding
191+
];
192+
193+
for (const str of invalidBase64Strings) {
194+
expect(str + z.string().base64().safeParse(str).success).toBe(str + "false");
195+
}
196+
})
197+
165198
test("url validations", () => {
166199
const url = z.string().url();
167200
url.parse("http://google.com");

deno/lib/types.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,8 @@ export type ZodStringCheck =
537537
precision: number | null;
538538
message?: string;
539539
}
540-
| { kind: "ip"; version?: IpVersion; message?: string };
540+
| { kind: "ip"; version?: IpVersion; message?: string }
541+
| { kind: "base64"; message?: string };
541542

542543
export interface ZodStringDef extends ZodTypeDef {
543544
checks: ZodStringCheck[];
@@ -579,6 +580,10 @@ const ipv4Regex =
579580
const ipv6Regex =
580581
/^(([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})))$/;
581582

583+
// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript
584+
const base64Regex =
585+
/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
586+
582587
// Adapted from https://stackoverflow.com/a/3143231
583588
const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
584589
if (args.precision) {
@@ -845,6 +850,16 @@ export class ZodString extends ZodType<string, ZodStringDef> {
845850
});
846851
status.dirty();
847852
}
853+
} else if (check.kind === "base64") {
854+
if (!base64Regex.test(input.data)) {
855+
ctx = this._getOrReturnCtx(input, ctx);
856+
addIssueToContext(ctx, {
857+
validation: "base64",
858+
code: ZodIssueCode.invalid_string,
859+
message: check.message,
860+
});
861+
status.dirty();
862+
}
848863
} else {
849864
util.assertNever(check);
850865
}
@@ -893,6 +908,9 @@ export class ZodString extends ZodType<string, ZodStringDef> {
893908
ulid(message?: errorUtil.ErrMessage) {
894909
return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) });
895910
}
911+
base64(message?: errorUtil.ErrMessage) {
912+
return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) });
913+
}
896914

897915
ip(options?: string | { version?: "v4" | "v6"; message?: string }) {
898916
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
@@ -1038,6 +1056,9 @@ export class ZodString extends ZodType<string, ZodStringDef> {
10381056
get isIP() {
10391057
return !!this._def.checks.find((ch) => ch.kind === "ip");
10401058
}
1059+
get isBase64() {
1060+
return !!this._def.checks.find((ch) => ch.kind === "base64");
1061+
}
10411062

10421063
get minLength() {
10431064
let min: number | null = null;

src/ZodError.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export type StringValidation =
9999
| "ulid"
100100
| "datetime"
101101
| "ip"
102+
| "base64"
102103
| { includes: string; position?: number }
103104
| { startsWith: string }
104105
| { endsWith: string };

src/__tests__/string.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,39 @@ test("email validations", () => {
161161
).toBe(true);
162162
});
163163

164+
test("base64 validations", () => {
165+
const validBase64Strings = [
166+
"SGVsbG8gV29ybGQ=", // "Hello World"
167+
"VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", // "This is an encoded string"
168+
"TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=", // "Many hands make light work"
169+
"UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // "Patience is the key to success"
170+
"QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // "Base64 encoding is fun"
171+
"MTIzNDU2Nzg5MA==", // "1234567890"
172+
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz"
173+
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=", // "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
174+
"ISIkJSMmJyonKCk=", // "!\"#$%&'()*"
175+
"" // Empty string is technically a valid base64
176+
];
177+
178+
for (const str of validBase64Strings) {
179+
expect(str + z.string().base64().safeParse(str).success).toBe(str + "true");
180+
}
181+
182+
const invalidBase64Strings = [
183+
"12345", // Not padded correctly, not a multiple of 4 characters
184+
"SGVsbG8gV29ybGQ", // Missing padding
185+
"VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw", // Missing padding
186+
"!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!'
187+
"?QmFzZTY0IGVuY29kaW5nIGlzIGZ1bg==", // Invalid character '?'
188+
".MTIzND2Nzg5MC4=", // Invalid character '.'
189+
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo", // Missing padding
190+
];
191+
192+
for (const str of invalidBase64Strings) {
193+
expect(str + z.string().base64().safeParse(str).success).toBe(str + "false");
194+
}
195+
})
196+
164197
test("url validations", () => {
165198
const url = z.string().url();
166199
url.parse("http://google.com");

src/types.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,8 @@ export type ZodStringCheck =
537537
precision: number | null;
538538
message?: string;
539539
}
540-
| { kind: "ip"; version?: IpVersion; message?: string };
540+
| { kind: "ip"; version?: IpVersion; message?: string }
541+
| { kind: "base64"; message?: string };
541542

542543
export interface ZodStringDef extends ZodTypeDef {
543544
checks: ZodStringCheck[];
@@ -579,6 +580,10 @@ const ipv4Regex =
579580
const ipv6Regex =
580581
/^(([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})))$/;
581582

583+
// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript
584+
const base64Regex =
585+
/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
586+
582587
// Adapted from https://stackoverflow.com/a/3143231
583588
const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
584589
if (args.precision) {
@@ -845,6 +850,16 @@ export class ZodString extends ZodType<string, ZodStringDef> {
845850
});
846851
status.dirty();
847852
}
853+
} else if (check.kind === "base64") {
854+
if (!base64Regex.test(input.data)) {
855+
ctx = this._getOrReturnCtx(input, ctx);
856+
addIssueToContext(ctx, {
857+
validation: "base64",
858+
code: ZodIssueCode.invalid_string,
859+
message: check.message,
860+
});
861+
status.dirty();
862+
}
848863
} else {
849864
util.assertNever(check);
850865
}
@@ -893,6 +908,9 @@ export class ZodString extends ZodType<string, ZodStringDef> {
893908
ulid(message?: errorUtil.ErrMessage) {
894909
return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) });
895910
}
911+
base64(message?: errorUtil.ErrMessage) {
912+
return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) });
913+
}
896914

897915
ip(options?: string | { version?: "v4" | "v6"; message?: string }) {
898916
return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
@@ -1038,6 +1056,9 @@ export class ZodString extends ZodType<string, ZodStringDef> {
10381056
get isIP() {
10391057
return !!this._def.checks.find((ch) => ch.kind === "ip");
10401058
}
1059+
get isBase64() {
1060+
return !!this._def.checks.find((ch) => ch.kind === "base64");
1061+
}
10411062

10421063
get minLength() {
10431064
let min: number | null = null;

0 commit comments

Comments
 (0)