123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- import { setResponseValueAndErrors } from "../errorMessages.js";
- let emojiRegex = undefined;
- /**
- * Generated from the regular expressions found here as of 2024-05-22:
- * https://github.com/colinhacks/zod/blob/master/src/types.ts.
- *
- * Expressions with /i flag have been changed accordingly.
- */
- export const zodPatterns = {
- /**
- * `c` was changed to `[cC]` to replicate /i flag
- */
- cuid: /^[cC][^\s-]{8,}$/,
- cuid2: /^[0-9a-z]+$/,
- ulid: /^[0-9A-HJKMNP-TV-Z]{26}$/,
- /**
- * `a-z` was added to replicate /i flag
- */
- email: /^(?!\.)(?!.*\.\.)([a-zA-Z0-9_'+\-\.]*)[a-zA-Z0-9_+-]@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/,
- /**
- * Constructed a valid Unicode RegExp
- *
- * Lazily instantiate since this type of regex isn't supported
- * in all envs (e.g. React Native).
- *
- * See:
- * https://github.com/colinhacks/zod/issues/2433
- * Fix in Zod:
- * https://github.com/colinhacks/zod/commit/9340fd51e48576a75adc919bff65dbc4a5d4c99b
- */
- emoji: () => {
- if (emojiRegex === undefined) {
- emojiRegex = RegExp("^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$", "u");
- }
- return emojiRegex;
- },
- /**
- * Unused
- */
- uuid: /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
- /**
- * Unused
- */
- ipv4: /^(?:(?: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])$/,
- ipv4Cidr: /^(?:(?: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])\/(3[0-2]|[12]?[0-9])$/,
- /**
- * Unused
- */
- ipv6: /^(([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})))$/,
- ipv6Cidr: /^(([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]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,
- base64: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,
- base64url: /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/,
- nanoid: /^[a-zA-Z0-9_-]{21}$/,
- jwt: /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/,
- };
- export function parseStringDef(def, refs) {
- const res = {
- type: "string",
- };
- if (def.checks) {
- for (const check of def.checks) {
- switch (check.kind) {
- case "min":
- setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number"
- ? Math.max(res.minLength, check.value)
- : check.value, check.message, refs);
- break;
- case "max":
- setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number"
- ? Math.min(res.maxLength, check.value)
- : check.value, check.message, refs);
- break;
- case "email":
- switch (refs.emailStrategy) {
- case "format:email":
- addFormat(res, "email", check.message, refs);
- break;
- case "format:idn-email":
- addFormat(res, "idn-email", check.message, refs);
- break;
- case "pattern:zod":
- addPattern(res, zodPatterns.email, check.message, refs);
- break;
- }
- break;
- case "url":
- addFormat(res, "uri", check.message, refs);
- break;
- case "uuid":
- addFormat(res, "uuid", check.message, refs);
- break;
- case "regex":
- addPattern(res, check.regex, check.message, refs);
- break;
- case "cuid":
- addPattern(res, zodPatterns.cuid, check.message, refs);
- break;
- case "cuid2":
- addPattern(res, zodPatterns.cuid2, check.message, refs);
- break;
- case "startsWith":
- addPattern(res, RegExp(`^${escapeLiteralCheckValue(check.value, refs)}`), check.message, refs);
- break;
- case "endsWith":
- addPattern(res, RegExp(`${escapeLiteralCheckValue(check.value, refs)}$`), check.message, refs);
- break;
- case "datetime":
- addFormat(res, "date-time", check.message, refs);
- break;
- case "date":
- addFormat(res, "date", check.message, refs);
- break;
- case "time":
- addFormat(res, "time", check.message, refs);
- break;
- case "duration":
- addFormat(res, "duration", check.message, refs);
- break;
- case "length":
- setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number"
- ? Math.max(res.minLength, check.value)
- : check.value, check.message, refs);
- setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number"
- ? Math.min(res.maxLength, check.value)
- : check.value, check.message, refs);
- break;
- case "includes": {
- addPattern(res, RegExp(escapeLiteralCheckValue(check.value, refs)), check.message, refs);
- break;
- }
- case "ip": {
- if (check.version !== "v6") {
- addFormat(res, "ipv4", check.message, refs);
- }
- if (check.version !== "v4") {
- addFormat(res, "ipv6", check.message, refs);
- }
- break;
- }
- case "base64url":
- addPattern(res, zodPatterns.base64url, check.message, refs);
- break;
- case "jwt":
- addPattern(res, zodPatterns.jwt, check.message, refs);
- break;
- case "cidr": {
- if (check.version !== "v6") {
- addPattern(res, zodPatterns.ipv4Cidr, check.message, refs);
- }
- if (check.version !== "v4") {
- addPattern(res, zodPatterns.ipv6Cidr, check.message, refs);
- }
- break;
- }
- case "emoji":
- addPattern(res, zodPatterns.emoji(), check.message, refs);
- break;
- case "ulid": {
- addPattern(res, zodPatterns.ulid, check.message, refs);
- break;
- }
- case "base64": {
- switch (refs.base64Strategy) {
- case "format:binary": {
- addFormat(res, "binary", check.message, refs);
- break;
- }
- case "contentEncoding:base64": {
- setResponseValueAndErrors(res, "contentEncoding", "base64", check.message, refs);
- break;
- }
- case "pattern:zod": {
- addPattern(res, zodPatterns.base64, check.message, refs);
- break;
- }
- }
- break;
- }
- case "nanoid": {
- addPattern(res, zodPatterns.nanoid, check.message, refs);
- }
- case "toLowerCase":
- case "toUpperCase":
- case "trim":
- break;
- default:
- /* c8 ignore next */
- ((_) => { })(check);
- }
- }
- }
- return res;
- }
- function escapeLiteralCheckValue(literal, refs) {
- return refs.patternStrategy === "escape"
- ? escapeNonAlphaNumeric(literal)
- : literal;
- }
- const ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
- function escapeNonAlphaNumeric(source) {
- let result = "";
- for (let i = 0; i < source.length; i++) {
- if (!ALPHA_NUMERIC.has(source[i])) {
- result += "\\";
- }
- result += source[i];
- }
- return result;
- }
- // Adds a "format" keyword to the schema. If a format exists, both formats will be joined in an allOf-node, along with subsequent ones.
- function addFormat(schema, value, message, refs) {
- if (schema.format || schema.anyOf?.some((x) => x.format)) {
- if (!schema.anyOf) {
- schema.anyOf = [];
- }
- if (schema.format) {
- schema.anyOf.push({
- format: schema.format,
- ...(schema.errorMessage &&
- refs.errorMessages && {
- errorMessage: { format: schema.errorMessage.format },
- }),
- });
- delete schema.format;
- if (schema.errorMessage) {
- delete schema.errorMessage.format;
- if (Object.keys(schema.errorMessage).length === 0) {
- delete schema.errorMessage;
- }
- }
- }
- schema.anyOf.push({
- format: value,
- ...(message &&
- refs.errorMessages && { errorMessage: { format: message } }),
- });
- }
- else {
- setResponseValueAndErrors(schema, "format", value, message, refs);
- }
- }
- // Adds a "pattern" keyword to the schema. If a pattern exists, both patterns will be joined in an allOf-node, along with subsequent ones.
- function addPattern(schema, regex, message, refs) {
- if (schema.pattern || schema.allOf?.some((x) => x.pattern)) {
- if (!schema.allOf) {
- schema.allOf = [];
- }
- if (schema.pattern) {
- schema.allOf.push({
- pattern: schema.pattern,
- ...(schema.errorMessage &&
- refs.errorMessages && {
- errorMessage: { pattern: schema.errorMessage.pattern },
- }),
- });
- delete schema.pattern;
- if (schema.errorMessage) {
- delete schema.errorMessage.pattern;
- if (Object.keys(schema.errorMessage).length === 0) {
- delete schema.errorMessage;
- }
- }
- }
- schema.allOf.push({
- pattern: stringifyRegExpWithFlags(regex, refs),
- ...(message &&
- refs.errorMessages && { errorMessage: { pattern: message } }),
- });
- }
- else {
- setResponseValueAndErrors(schema, "pattern", stringifyRegExpWithFlags(regex, refs), message, refs);
- }
- }
- // Mutate z.string.regex() in a best attempt to accommodate for regex flags when applyRegexFlags is true
- function stringifyRegExpWithFlags(regex, refs) {
- if (!refs.applyRegexFlags || !regex.flags) {
- return regex.source;
- }
- // Currently handled flags
- const flags = {
- i: regex.flags.includes("i"),
- m: regex.flags.includes("m"),
- s: regex.flags.includes("s"), // `.` matches newlines
- };
- // The general principle here is to step through each character, one at a time, applying mutations as flags require. We keep track when the current character is escaped, and when it's inside a group /like [this]/ or (also) a range like /[a-z]/. The following is fairly brittle imperative code; edit at your peril!
- const source = flags.i ? regex.source.toLowerCase() : regex.source;
- let pattern = "";
- let isEscaped = false;
- let inCharGroup = false;
- let inCharRange = false;
- for (let i = 0; i < source.length; i++) {
- if (isEscaped) {
- pattern += source[i];
- isEscaped = false;
- continue;
- }
- if (flags.i) {
- if (inCharGroup) {
- if (source[i].match(/[a-z]/)) {
- if (inCharRange) {
- pattern += source[i];
- pattern += `${source[i - 2]}-${source[i]}`.toUpperCase();
- inCharRange = false;
- }
- else if (source[i + 1] === "-" && source[i + 2]?.match(/[a-z]/)) {
- pattern += source[i];
- inCharRange = true;
- }
- else {
- pattern += `${source[i]}${source[i].toUpperCase()}`;
- }
- continue;
- }
- }
- else if (source[i].match(/[a-z]/)) {
- pattern += `[${source[i]}${source[i].toUpperCase()}]`;
- continue;
- }
- }
- if (flags.m) {
- if (source[i] === "^") {
- pattern += `(^|(?<=[\r\n]))`;
- continue;
- }
- else if (source[i] === "$") {
- pattern += `($|(?=[\r\n]))`;
- continue;
- }
- }
- if (flags.s && source[i] === ".") {
- pattern += inCharGroup ? `${source[i]}\r\n` : `[${source[i]}\r\n]`;
- continue;
- }
- pattern += source[i];
- if (source[i] === "\\") {
- isEscaped = true;
- }
- else if (inCharGroup && source[i] === "]") {
- inCharGroup = false;
- }
- else if (!inCharGroup && source[i] === "[") {
- inCharGroup = true;
- }
- }
- try {
- new RegExp(pattern);
- }
- catch {
- console.warn(`Could not convert regex pattern at ${refs.currentPath.join("/")} to a flag-independent form! Falling back to the flag-ignorant source`);
- return regex.source;
- }
- return pattern;
- }
|