z-schema 12.3.1 → 12.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/z-schema +0 -0
- package/cjs/index.cjs +151 -29
- package/dist/format-validators.js +20 -10
- package/dist/json-schema.js +9 -3
- package/dist/json-validation.js +3 -2
- package/dist/report.d.ts +1 -1
- package/dist/schema-cache.d.ts +1 -1
- package/dist/schema-compiler.js +8 -4
- package/dist/utils/hostname.js +109 -5
- package/dist/utils/json.js +2 -1
- package/dist/utils/time.js +3 -3
- package/dist/validation/ref.js +1 -1
- package/dist/validation/shared.js +4 -2
- package/dist/validation/string.js +4 -4
- package/dist/z-schema-options.js +1 -1
- package/dist/z-schema.js +1 -1
- package/package.json +5 -5
- package/src/format-validators.ts +36 -10
- package/src/json-validation.ts +7 -7
- package/src/utils/base64.ts +1 -1
- package/src/utils/hostname.ts +230 -4
- package/src/utils/time.ts +3 -2
- package/src/validation/string.ts +7 -3
- package/umd/ZSchema.js +151 -29
- package/umd/ZSchema.min.js +2 -2
package/dist/utils/hostname.js
CHANGED
|
@@ -3,6 +3,94 @@ import punycode from "punycode/punycode.js";
|
|
|
3
3
|
//#region src/utils/hostname.ts
|
|
4
4
|
const IDN_SEPARATOR_REGEX = /[\u3002\uFF0E\uFF61]/g;
|
|
5
5
|
const IDN_SEPARATOR_TEST_REGEX = /[\u3002\uFF0E\uFF61]/;
|
|
6
|
+
const XN_LABEL_REGEX = /^xn--/i;
|
|
7
|
+
const DISALLOWED_CATEGORY_REGEX = /[\p{P}\p{S}\p{Z}\p{C}]/u;
|
|
8
|
+
const DISALLOWED_CATEGORY_WHITELIST = /* @__PURE__ */ new Set([
|
|
9
|
+
"-",
|
|
10
|
+
"·",
|
|
11
|
+
"͵",
|
|
12
|
+
"׳",
|
|
13
|
+
"״",
|
|
14
|
+
"・",
|
|
15
|
+
"۽",
|
|
16
|
+
"۾",
|
|
17
|
+
"་",
|
|
18
|
+
"",
|
|
19
|
+
""
|
|
20
|
+
]);
|
|
21
|
+
const isDisallowedCodePoint = (char) => DISALLOWED_CATEGORY_REGEX.test(char) && !DISALLOWED_CATEGORY_WHITELIST.has(char);
|
|
22
|
+
const TRANSPARENT_MARK_REGEX = /\p{Mn}/u;
|
|
23
|
+
const ARABIC_SCRIPT_REGEX = /\p{Script=Arabic}/u;
|
|
24
|
+
const ARABIC_INDIC_DIGITS_REGEX = /[\u0660-\u0669\u06F0-\u06F9]/;
|
|
25
|
+
const isArabicJoiningLetter = (char) => char !== void 0 && ARABIC_SCRIPT_REGEX.test(char) && !ARABIC_INDIC_DIGITS_REGEX.test(char);
|
|
26
|
+
const hasZwnjJoiningContext = (label, idx) => {
|
|
27
|
+
let before = idx - 1;
|
|
28
|
+
while (before >= 0 && TRANSPARENT_MARK_REGEX.test(label[before])) before--;
|
|
29
|
+
let after = idx + 1;
|
|
30
|
+
while (after < label.length && TRANSPARENT_MARK_REGEX.test(label[after])) after++;
|
|
31
|
+
return isArabicJoiningLetter(label[before]) && isArabicJoiningLetter(label[after]);
|
|
32
|
+
};
|
|
33
|
+
const BIDI_NSM_REGEX = /[\p{Mn}\p{Me}]/u;
|
|
34
|
+
const BIDI_ARABIC_INDIC_DIGIT_REGEX = /[\u0660-\u0669]/;
|
|
35
|
+
const BIDI_EXTENDED_ARABIC_INDIC_DIGIT_REGEX = /[\u06F0-\u06F9]/;
|
|
36
|
+
const BIDI_HEBREW_SCRIPT_REGEX = /\p{Script=Hebrew}/u;
|
|
37
|
+
const BIDI_AL_SCRIPT_REGEX = /[\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}]/u;
|
|
38
|
+
const BIDI_ASCII_DIGIT_REGEX = /[0-9]/;
|
|
39
|
+
const BIDI_LETTER_REGEX = /\p{L}/u;
|
|
40
|
+
const classifyBidi = (char) => {
|
|
41
|
+
if (BIDI_NSM_REGEX.test(char)) return "NSM";
|
|
42
|
+
if (BIDI_ARABIC_INDIC_DIGIT_REGEX.test(char)) return "AN";
|
|
43
|
+
if (BIDI_EXTENDED_ARABIC_INDIC_DIGIT_REGEX.test(char)) return "EN";
|
|
44
|
+
if (BIDI_HEBREW_SCRIPT_REGEX.test(char)) return "R";
|
|
45
|
+
if (BIDI_AL_SCRIPT_REGEX.test(char)) return "AL";
|
|
46
|
+
if (BIDI_ASCII_DIGIT_REGEX.test(char)) return "EN";
|
|
47
|
+
if (BIDI_LETTER_REGEX.test(char)) return "L";
|
|
48
|
+
return "ON";
|
|
49
|
+
};
|
|
50
|
+
const isLabelBidiTriggering = (label) => {
|
|
51
|
+
for (const char of label) {
|
|
52
|
+
const bidiClass = classifyBidi(char);
|
|
53
|
+
if (bidiClass === "R" || bidiClass === "AL" || bidiClass === "AN") return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
const isLabelBidiRuleValid = (label) => {
|
|
58
|
+
const classes = [];
|
|
59
|
+
for (const char of label) classes.push(classifyBidi(char));
|
|
60
|
+
if (classes.length === 0) return false;
|
|
61
|
+
const first = classes[0];
|
|
62
|
+
if (first !== "L" && first !== "R" && first !== "AL") return false;
|
|
63
|
+
let lastIdx = classes.length - 1;
|
|
64
|
+
while (lastIdx > 0 && classes[lastIdx] === "NSM") lastIdx--;
|
|
65
|
+
const last = classes[lastIdx];
|
|
66
|
+
if (first === "R" || first === "AL") {
|
|
67
|
+
let hasEN = false;
|
|
68
|
+
let hasAN = false;
|
|
69
|
+
for (let i = 0; i < classes.length; i++) {
|
|
70
|
+
const c = classes[i];
|
|
71
|
+
if (c !== "R" && c !== "AL" && c !== "AN" && c !== "EN" && c !== "NSM" && c !== "ON") return false;
|
|
72
|
+
if (c === "EN") hasEN = true;
|
|
73
|
+
else if (c === "AN") hasAN = true;
|
|
74
|
+
}
|
|
75
|
+
if (hasEN && hasAN) return false;
|
|
76
|
+
return last === "R" || last === "AL" || last === "EN" || last === "AN";
|
|
77
|
+
}
|
|
78
|
+
for (let i = 0; i < classes.length; i++) {
|
|
79
|
+
const c = classes[i];
|
|
80
|
+
if (c !== "L" && c !== "EN" && c !== "NSM" && c !== "ON") return false;
|
|
81
|
+
}
|
|
82
|
+
return last === "L" || last === "EN";
|
|
83
|
+
};
|
|
84
|
+
const isDomainBidiValid = (unicodeLabels) => {
|
|
85
|
+
let isBidiDomain = false;
|
|
86
|
+
for (let i = 0; i < unicodeLabels.length; i++) if (isLabelBidiTriggering(unicodeLabels[i])) {
|
|
87
|
+
isBidiDomain = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (!isBidiDomain) return true;
|
|
91
|
+
for (let i = 0; i < unicodeLabels.length; i++) if (!isLabelBidiRuleValid(unicodeLabels[i])) return false;
|
|
92
|
+
return true;
|
|
93
|
+
};
|
|
6
94
|
const splitHostnameLabels = (hostname) => {
|
|
7
95
|
if (hostname.length === 0 || hostname.length > 255) return null;
|
|
8
96
|
if (hostname.startsWith(".") || hostname.endsWith(".")) return null;
|
|
@@ -18,7 +106,7 @@ const isGreek = (char) => /\p{Script=Greek}/u.test(char);
|
|
|
18
106
|
const isHebrew = (char) => /\p{Script=Hebrew}/u.test(char);
|
|
19
107
|
const hasCjkKanaOrHan = (input) => /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(input);
|
|
20
108
|
const toUnicodeLabel = (label) => {
|
|
21
|
-
if (
|
|
109
|
+
if (!XN_LABEL_REGEX.test(label)) return label;
|
|
22
110
|
try {
|
|
23
111
|
return punycode.toUnicode(label.toLowerCase());
|
|
24
112
|
} catch {
|
|
@@ -27,7 +115,7 @@ const toUnicodeLabel = (label) => {
|
|
|
27
115
|
};
|
|
28
116
|
const isValidIdnUnicodeLabel = (label) => {
|
|
29
117
|
if (label.startsWith("-") || label.endsWith("-")) return false;
|
|
30
|
-
if (label.length >= 4 && label[2] === "-" && label[3] === "-" &&
|
|
118
|
+
if (label.length >= 4 && label[2] === "-" && label[3] === "-" && !XN_LABEL_REGEX.test(label)) return false;
|
|
31
119
|
if (/^\p{M}/u.test(label)) return false;
|
|
32
120
|
if (/[\u302E\u302F\u0640\u07FA]/u.test(label)) return false;
|
|
33
121
|
for (let idx = 0; idx < label.length; idx++) {
|
|
@@ -36,7 +124,9 @@ const isValidIdnUnicodeLabel = (label) => {
|
|
|
36
124
|
if (char === "͵" && (idx === label.length - 1 || !isGreek(label[idx + 1]))) return false;
|
|
37
125
|
if ((char === "׳" || char === "״") && (idx === 0 || !isHebrew(label[idx - 1]))) return false;
|
|
38
126
|
if (char === "" && (idx === 0 || label[idx - 1] !== "्")) return false;
|
|
127
|
+
if (char === "" && (idx === 0 || label[idx - 1] !== "्" && !hasZwnjJoiningContext(label, idx))) return false;
|
|
39
128
|
}
|
|
129
|
+
for (const char of label) if (isDisallowedCodePoint(char)) return false;
|
|
40
130
|
if (label.includes("・") && !hasCjkKanaOrHan(label.replaceAll("・", ""))) return false;
|
|
41
131
|
const hasArabicIndic = /[\u0660-\u0669]/.test(label);
|
|
42
132
|
const hasExtendedArabicIndic = /[\u06F0-\u06F9]/.test(label);
|
|
@@ -51,7 +141,7 @@ const isValidHostname = (hostname) => {
|
|
|
51
141
|
for (let i = 0; i < labels.length; i++) {
|
|
52
142
|
const label = labels[i];
|
|
53
143
|
if (!isAsciiHostnameLabel(label)) return false;
|
|
54
|
-
if (
|
|
144
|
+
if (XN_LABEL_REGEX.test(label)) {
|
|
55
145
|
const unicodeLabel = toUnicodeLabel(label);
|
|
56
146
|
if (unicodeLabel === null || !isValidIdnUnicodeLabel(unicodeLabel)) return false;
|
|
57
147
|
}
|
|
@@ -59,14 +149,28 @@ const isValidHostname = (hostname) => {
|
|
|
59
149
|
return true;
|
|
60
150
|
};
|
|
61
151
|
const isValidIdnHostname = (hostname) => {
|
|
62
|
-
const
|
|
152
|
+
const normalizedHostname = hostname.replace(IDN_SEPARATOR_REGEX, ".");
|
|
153
|
+
const labels = splitHostnameLabels(normalizedHostname);
|
|
63
154
|
if (labels === null) return false;
|
|
155
|
+
const unicodeLabels = [];
|
|
156
|
+
let totalLength = labels.length - 1;
|
|
64
157
|
for (let i = 0; i < labels.length; i++) {
|
|
65
158
|
const label = labels[i];
|
|
66
159
|
const unicodeLabel = toUnicodeLabel(label);
|
|
67
160
|
if (unicodeLabel === null || !isValidIdnUnicodeLabel(unicodeLabel)) return false;
|
|
161
|
+
let aLabel;
|
|
162
|
+
try {
|
|
163
|
+
aLabel = punycode.toASCII(unicodeLabel);
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (XN_LABEL_REGEX.test(label) && aLabel.toLowerCase() !== label.toLowerCase()) return false;
|
|
168
|
+
if (aLabel.length > 63) return false;
|
|
169
|
+
totalLength += aLabel.length;
|
|
170
|
+
unicodeLabels.push(unicodeLabel);
|
|
68
171
|
}
|
|
69
|
-
return
|
|
172
|
+
if (totalLength > 253) return false;
|
|
173
|
+
return isDomainBidiValid(unicodeLabels);
|
|
70
174
|
};
|
|
71
175
|
//#endregion
|
|
72
176
|
export { isValidHostname, isValidIdnHostname };
|
package/dist/utils/json.js
CHANGED
|
@@ -16,7 +16,8 @@ const areEqual = (json1, json2, options, _depth = 0) => {
|
|
|
16
16
|
}
|
|
17
17
|
if (isObject(json1) && isObject(json2)) {
|
|
18
18
|
const keys1 = sortedKeys(json1);
|
|
19
|
-
|
|
19
|
+
const keys2 = sortedKeys(json2);
|
|
20
|
+
if (!areEqual(keys1, keys2, options, _depth + 1)) return false;
|
|
20
21
|
len = keys1.length;
|
|
21
22
|
for (i = 0; i < len; i++) if (!areEqual(json1[keys1[i]], json2[keys1[i]], options, _depth + 1)) return false;
|
|
22
23
|
return true;
|
package/dist/utils/time.js
CHANGED
|
@@ -8,7 +8,7 @@ const toUtcTime = (hour, minute, offsetSign, offsetHour, offsetMinute) => {
|
|
|
8
8
|
minute: normalizedUtcTotalMinutes % 60
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
|
-
const RFC3339_TIME_REGEX = /^([0-9]{2}):([0-9]{2}):([0-9]{2})(
|
|
11
|
+
const RFC3339_TIME_REGEX = /^(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})(?<fraction>\.[0-9]+)?(?<offset>z|(?<offsetValue>[+-][0-9]{2}:[0-9]{2}))$/i;
|
|
12
12
|
const parseRfc3339Time = (time) => {
|
|
13
13
|
const matches = RFC3339_TIME_REGEX.exec(time);
|
|
14
14
|
if (matches === null) return null;
|
|
@@ -19,7 +19,7 @@ const parseRfc3339Time = (time) => {
|
|
|
19
19
|
let utcHour = hour;
|
|
20
20
|
let utcMinute = minute;
|
|
21
21
|
if (matches[5].toLowerCase() !== "z") {
|
|
22
|
-
const offsetMatches = /^([+-])([0-9]{2}):([0-9]{2})$/.exec(matches[5]);
|
|
22
|
+
const offsetMatches = /^(?<sign>[+-])(?<hour>[0-9]{2}):(?<minute>[0-9]{2})$/.exec(matches[5]);
|
|
23
23
|
if (offsetMatches === null) return null;
|
|
24
24
|
const offsetSign = offsetMatches[1];
|
|
25
25
|
const offsetHour = Number.parseInt(offsetMatches[2], 10);
|
|
@@ -39,4 +39,4 @@ const parseRfc3339Time = (time) => {
|
|
|
39
39
|
};
|
|
40
40
|
};
|
|
41
41
|
//#endregion
|
|
42
|
-
export { parseRfc3339Time };
|
|
42
|
+
export { parseRfc3339Time, toUtcTime };
|
package/dist/validation/ref.js
CHANGED
|
@@ -9,7 +9,7 @@ const VOCAB_VALIDATION_2019_09 = "https://json-schema.org/draft/2019-09/vocab/va
|
|
|
9
9
|
const VOCAB_VALIDATION_2020_12 = "https://json-schema.org/draft/2020-12/vocab/validation";
|
|
10
10
|
const VOCAB_FORMAT_2019_09 = "https://json-schema.org/draft/2019-09/vocab/format";
|
|
11
11
|
const VOCAB_FORMAT_ASSERTION_2020_12 = "https://json-schema.org/draft/2020-12/vocab/format-assertion";
|
|
12
|
-
const VALIDATION_VOCAB_KEYWORDS = new Set([
|
|
12
|
+
const VALIDATION_VOCAB_KEYWORDS = /* @__PURE__ */ new Set([
|
|
13
13
|
"type",
|
|
14
14
|
"multipleOf",
|
|
15
15
|
"maximum",
|
|
@@ -40,7 +40,9 @@ const isValidationVocabularyEnabled = (schema, report, version) => {
|
|
|
40
40
|
const metaSchema = currentSchemaMeta || rootSchemaMeta;
|
|
41
41
|
if (!metaSchema || typeof metaSchema !== "object" || !isObject(metaSchema.$vocabulary)) return true;
|
|
42
42
|
const vocabulary = metaSchema.$vocabulary;
|
|
43
|
-
|
|
43
|
+
const has2019 = Object.hasOwn(vocabulary, VOCAB_VALIDATION_2019_09);
|
|
44
|
+
const has2020 = Object.hasOwn(vocabulary, VOCAB_VALIDATION_2020_12);
|
|
45
|
+
if (has2019 || has2020) return vocabulary[VOCAB_VALIDATION_2019_09] || vocabulary[VOCAB_VALIDATION_2020_12];
|
|
44
46
|
return false;
|
|
45
47
|
};
|
|
46
48
|
/**
|
|
@@ -45,11 +45,11 @@ function formatValidator(ctx, report, schema, json) {
|
|
|
45
45
|
if (result instanceof Promise) {
|
|
46
46
|
const promiseResult = result;
|
|
47
47
|
report.addAsyncTaskWithPath(async (callback) => {
|
|
48
|
+
let resolved = false;
|
|
48
49
|
try {
|
|
49
|
-
|
|
50
|
-
} catch {
|
|
51
|
-
|
|
52
|
-
}
|
|
50
|
+
resolved = await promiseResult;
|
|
51
|
+
} catch {}
|
|
52
|
+
callback(resolved);
|
|
53
53
|
}, [], (resolvedResult) => {
|
|
54
54
|
if (resolvedResult !== true) report.addError("INVALID_FORMAT", [schema.format, JSON.stringify(json)], void 0, schema, "format");
|
|
55
55
|
});
|
package/dist/z-schema-options.js
CHANGED
package/dist/z-schema.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-schema",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.0",
|
|
4
4
|
"description": "Fast, lightweight JSON Schema validator for Node.js and browsers — full support for draft-04, draft-06, draft-07, draft-2019-09, and draft-2020-12 (latest)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"draft-04",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@types/lodash.isequal": "^4.5.8",
|
|
85
|
-
"@types/node": "^
|
|
85
|
+
"@types/node": "^26.0.1",
|
|
86
86
|
"@types/punycode": "^2.1.4",
|
|
87
87
|
"@types/validator": "^13.15.10",
|
|
88
88
|
"@vitest/browser-playwright": "^4.1.0",
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
"@vitest/coverage-v8": "^4.1.0",
|
|
91
91
|
"lefthook": "^2.1.9",
|
|
92
92
|
"lodash.isequal": "^4.5.0",
|
|
93
|
-
"oxfmt": "^0.
|
|
93
|
+
"oxfmt": "^0.57.0",
|
|
94
94
|
"oxlint": "^1.56.0",
|
|
95
|
-
"oxlint-tsgolint": "^0.
|
|
95
|
+
"oxlint-tsgolint": "^0.24.0",
|
|
96
96
|
"rimraf": "^6.1.2",
|
|
97
97
|
"tsdown": "^0.22.0",
|
|
98
98
|
"typescript": "^6.0.3",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"vitest": "^4.1.0"
|
|
101
101
|
},
|
|
102
102
|
"optionalDependencies": {
|
|
103
|
-
"commander": "^
|
|
103
|
+
"commander": "^15.0.0"
|
|
104
104
|
},
|
|
105
105
|
"overrides": {
|
|
106
106
|
"vite": ">=7.3.2"
|
package/src/format-validators.ts
CHANGED
|
@@ -14,7 +14,7 @@ const dateValidator: FormatValidatorFn = (date: unknown) => {
|
|
|
14
14
|
return true;
|
|
15
15
|
}
|
|
16
16
|
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
|
|
17
|
-
const matches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(date);
|
|
17
|
+
const matches = /^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})$/.exec(date);
|
|
18
18
|
if (matches === null) {
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
@@ -39,7 +39,7 @@ const dateTimeValidator: FormatValidatorFn = (dateTime: unknown) => {
|
|
|
39
39
|
const datePart = dateTime.slice(0, tIdx);
|
|
40
40
|
const timePart = dateTime.slice(tIdx + 1);
|
|
41
41
|
// Check date
|
|
42
|
-
const dateMatches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(datePart);
|
|
42
|
+
const dateMatches = /^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})$/.exec(datePart);
|
|
43
43
|
if (dateMatches === null) {
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
@@ -61,7 +61,7 @@ const emailValidator: FormatValidatorFn = (email: unknown) => {
|
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const ipv6Literal = /^(
|
|
64
|
+
const ipv6Literal = /^(?<localPart>.+)@\[IPv6:(?<address>[^\]]+)\]$/i.exec(email);
|
|
65
65
|
if (!ipv6Literal) {
|
|
66
66
|
return false;
|
|
67
67
|
}
|
|
@@ -226,7 +226,7 @@ const uriValidator: FormatValidatorFn = (uri: unknown) => {
|
|
|
226
226
|
if (!hasValidPercentEncoding(uri)) {
|
|
227
227
|
return false;
|
|
228
228
|
}
|
|
229
|
-
const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)/.exec(uri);
|
|
229
|
+
const match = /^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?<authority>[^/?#]*)/.exec(uri);
|
|
230
230
|
if (match) {
|
|
231
231
|
const authority = match[2];
|
|
232
232
|
const atIndex = authority.indexOf('@');
|
|
@@ -267,7 +267,7 @@ const uriReferenceValidator: FormatValidatorFn = (uri: unknown) => {
|
|
|
267
267
|
return false;
|
|
268
268
|
}
|
|
269
269
|
// URI-reference allows relative URIs
|
|
270
|
-
return /^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/.test(uri);
|
|
270
|
+
return /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/.test(uri);
|
|
271
271
|
};
|
|
272
272
|
|
|
273
273
|
const uriTemplateValidator: FormatValidatorFn = (uri: unknown) => {
|
|
@@ -275,7 +275,7 @@ const uriTemplateValidator: FormatValidatorFn = (uri: unknown) => {
|
|
|
275
275
|
return true;
|
|
276
276
|
}
|
|
277
277
|
// URI template allows braces for expressions.
|
|
278
|
-
if (!/^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^`| ]*$/.test(uri)) {
|
|
278
|
+
if (!/^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^`| ]*$/.test(uri)) {
|
|
279
279
|
return false;
|
|
280
280
|
}
|
|
281
281
|
|
|
@@ -338,7 +338,7 @@ const relativeJsonPointerValidator: FormatValidatorFn = (pointer: unknown) => {
|
|
|
338
338
|
}
|
|
339
339
|
// Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
|
|
340
340
|
// followed by either '#', a JSON Pointer, or nothing.
|
|
341
|
-
const match = /^(0|[1-9]\d*)(
|
|
341
|
+
const match = /^(?<int>0|[1-9]\d*)(?<suffix>.*)$/.exec(pointer);
|
|
342
342
|
if (!match) {
|
|
343
343
|
return false;
|
|
344
344
|
}
|
|
@@ -368,12 +368,38 @@ const timeValidator: FormatValidatorFn = (time: unknown) => {
|
|
|
368
368
|
return parseRfc3339Time(time) !== null;
|
|
369
369
|
};
|
|
370
370
|
|
|
371
|
+
// Matches a lone (unpaired) UTF-16 surrogate — either a high surrogate not
|
|
372
|
+
// followed by a low surrogate, or a low surrogate not preceded by a high one.
|
|
373
|
+
const LONE_SURROGATE_REGEX = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
374
|
+
// A quoted local part: any char except a bare quote/backslash; backslash escapes
|
|
375
|
+
// allowed. Leniently permits raw control chars (e.g. bare CR/LF), which stricter
|
|
376
|
+
// RFC 5321 parsing forbids — an accepted limitation, not exercised by the suite.
|
|
377
|
+
const QUOTED_LOCAL_PART_REGEX = /^"(?:[^"\\]|\\.)*"$/u;
|
|
378
|
+
const UNQUOTED_LOCAL_PART_REGEX = /^[^\s@]+$/u;
|
|
379
|
+
|
|
380
|
+
const isValidIdnEmailLocalPart = (localPart: string): boolean => {
|
|
381
|
+
if (localPart.length === 0 || LONE_SURROGATE_REGEX.test(localPart)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
if (localPart.length >= 2 && localPart.startsWith('"') && localPart.endsWith('"')) {
|
|
385
|
+
return QUOTED_LOCAL_PART_REGEX.test(localPart);
|
|
386
|
+
}
|
|
387
|
+
return UNQUOTED_LOCAL_PART_REGEX.test(localPart);
|
|
388
|
+
};
|
|
389
|
+
|
|
371
390
|
const idnEmailValidator: FormatValidatorFn = (email: unknown) => {
|
|
372
391
|
if (typeof email !== 'string') {
|
|
373
392
|
return true;
|
|
374
393
|
}
|
|
375
|
-
//
|
|
376
|
-
|
|
394
|
+
// Split on the last '@': everything after it is the (idn-)hostname domain,
|
|
395
|
+
// everything before it is the local part (which may itself be quoted).
|
|
396
|
+
// Note: unlike the ASCII `email` validator, IP-literal domains
|
|
397
|
+
// (`user@[192.168.1.1]` / `@[IPv6:...]`) are not supported here.
|
|
398
|
+
const atIdx = email.lastIndexOf('@');
|
|
399
|
+
if (atIdx <= 0 || atIdx === email.length - 1) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return isValidIdnEmailLocalPart(email.slice(0, atIdx)) && isValidIdnHostname(email.slice(atIdx + 1));
|
|
377
403
|
};
|
|
378
404
|
|
|
379
405
|
const idnHostnameValidator: FormatValidatorFn = (hostname: unknown) => {
|
|
@@ -406,7 +432,7 @@ const iriReferenceValidator: FormatValidatorFn = (iriReference: unknown) => {
|
|
|
406
432
|
if (typeof iriReference !== 'string') {
|
|
407
433
|
return true;
|
|
408
434
|
}
|
|
409
|
-
return /^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/u.test(iriReference);
|
|
435
|
+
return /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/u.test(iriReference);
|
|
410
436
|
};
|
|
411
437
|
|
|
412
438
|
export interface FormatValidatorsOptions {
|
package/src/json-validation.ts
CHANGED
|
@@ -169,7 +169,7 @@ function collectEvaluated(ctx: ZSchemaBase, args: CollectEvaluatedArgs): Set<num
|
|
|
169
169
|
let passed = getCachedValidationResult(report, currentSchema.contains, jsonArr[i]);
|
|
170
170
|
if (passed === undefined) {
|
|
171
171
|
const subReport = new Report(report);
|
|
172
|
-
validate(ctx, subReport, currentSchema.contains
|
|
172
|
+
validate(ctx, subReport, currentSchema.contains, jsonArr[i]);
|
|
173
173
|
passed = subReport.errors.length === 0;
|
|
174
174
|
}
|
|
175
175
|
if (passed) {
|
|
@@ -264,7 +264,7 @@ function collectEvaluated(ctx: ZSchemaBase, args: CollectEvaluatedArgs): Set<num
|
|
|
264
264
|
// allOf
|
|
265
265
|
if (Array.isArray(currentSchema.allOf)) {
|
|
266
266
|
for (let i = 0; i < currentSchema.allOf.length; i++) {
|
|
267
|
-
if (merge(recurse(currentSchema.allOf[i]
|
|
267
|
+
if (merge(recurse(currentSchema.allOf[i]))) {
|
|
268
268
|
return 'all';
|
|
269
269
|
}
|
|
270
270
|
}
|
|
@@ -277,10 +277,10 @@ function collectEvaluated(ctx: ZSchemaBase, args: CollectEvaluatedArgs): Set<num
|
|
|
277
277
|
let passed = getCachedValidationResult(report, subSchema, json);
|
|
278
278
|
if (passed === undefined) {
|
|
279
279
|
const subReport = new Report(report);
|
|
280
|
-
validate(ctx, subReport, subSchema
|
|
280
|
+
validate(ctx, subReport, subSchema, json);
|
|
281
281
|
passed = subReport.errors.length === 0;
|
|
282
282
|
}
|
|
283
|
-
if (passed && merge(recurse(subSchema
|
|
283
|
+
if (passed && merge(recurse(subSchema))) {
|
|
284
284
|
return 'all';
|
|
285
285
|
}
|
|
286
286
|
}
|
|
@@ -293,10 +293,10 @@ function collectEvaluated(ctx: ZSchemaBase, args: CollectEvaluatedArgs): Set<num
|
|
|
293
293
|
let passed = getCachedValidationResult(report, subSchema, json);
|
|
294
294
|
if (passed === undefined) {
|
|
295
295
|
const subReport = new Report(report);
|
|
296
|
-
validate(ctx, subReport, subSchema
|
|
296
|
+
validate(ctx, subReport, subSchema, json);
|
|
297
297
|
passed = subReport.errors.length === 0;
|
|
298
298
|
}
|
|
299
|
-
if (passed && merge(recurse(subSchema
|
|
299
|
+
if (passed && merge(recurse(subSchema))) {
|
|
300
300
|
return 'all';
|
|
301
301
|
}
|
|
302
302
|
}
|
|
@@ -339,7 +339,7 @@ function collectEvaluated(ctx: ZSchemaBase, args: CollectEvaluatedArgs): Set<num
|
|
|
339
339
|
|
|
340
340
|
// $dynamicRef
|
|
341
341
|
const dynamicTarget = resolveDynamicRef(currentSchema, report.__$dynamicScopeStack);
|
|
342
|
-
if (dynamicTarget && dynamicTarget !== currentSchema && merge(recurse(dynamicTarget
|
|
342
|
+
if (dynamicTarget && dynamicTarget !== currentSchema && merge(recurse(dynamicTarget))) {
|
|
343
343
|
return 'all';
|
|
344
344
|
}
|
|
345
345
|
|
package/src/utils/base64.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const decodeBase64 = (value: string): string | undefined => {
|
|
|
28
28
|
// where `atob` above is used instead.)
|
|
29
29
|
const bufferCtor = (
|
|
30
30
|
globalThis as {
|
|
31
|
-
Buffer?: { from(data: string, encoding: string)
|
|
31
|
+
Buffer?: { from: (data: string, encoding: string) => { toString: (encoding: string) => string } };
|
|
32
32
|
}
|
|
33
33
|
).Buffer;
|
|
34
34
|
if (bufferCtor !== undefined) {
|