z-schema 12.2.0 → 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/README.md +2 -2
- package/bin/z-schema +1 -1
- package/cjs/{index.js → index.cjs} +838 -707
- package/cjs/{index.d.ts → index.d.cts} +47 -26
- package/dist/{errors.d.mts → errors.d.ts} +2 -2
- package/dist/{errors.mjs → errors.js} +1 -2
- package/dist/{format-validators.mjs → format-validators.js} +60 -43
- package/dist/{index.d.mts → index.d.ts} +9 -9
- package/dist/{index.mjs → index.js} +3 -3
- package/dist/{json-schema-versions.d.mts → json-schema-versions.d.ts} +34 -3
- package/dist/{json-schema.d.mts → json-schema.d.ts} +7 -7
- package/dist/{json-schema.mjs → json-schema.js} +6 -5
- package/dist/{json-validation.mjs → json-validation.js} +144 -127
- package/dist/{report.d.mts → report.d.ts} +8 -9
- package/dist/{report.mjs → report.js} +28 -31
- package/dist/{schema-cache.d.mts → schema-cache.d.ts} +5 -5
- package/dist/{schema-cache.mjs → schema-cache.js} +10 -11
- package/dist/{schema-compiler.d.mts → schema-compiler.d.ts} +4 -4
- package/dist/{schema-compiler.mjs → schema-compiler.js} +101 -79
- package/dist/{schema-validator.d.mts → schema-validator.d.ts} +5 -5
- package/dist/{schema-validator.mjs → schema-validator.js} +138 -166
- package/dist/utils/{array.mjs → array.js} +4 -3
- package/dist/utils/{base64.mjs → base64.js} +3 -2
- package/dist/utils/{clone.mjs → clone.js} +18 -20
- package/dist/utils/hostname.js +176 -0
- package/dist/utils/{json.mjs → json.js} +13 -8
- package/dist/utils/{schema-regex.mjs → schema-regex.js} +5 -5
- package/dist/utils/{time.mjs → time.js} +8 -8
- package/dist/utils/unicode.js +22 -0
- package/dist/utils/{what-is.mjs → what-is.js} +1 -2
- package/dist/validation/{array.mjs → array.js} +18 -20
- package/dist/validation/{combinators.mjs → combinators.js} +16 -16
- package/dist/validation/{numeric.mjs → numeric.js} +11 -11
- package/dist/validation/{object.mjs → object.js} +35 -34
- package/dist/validation/{ref.mjs → ref.js} +5 -5
- package/dist/validation/{shared.mjs → shared.js} +15 -12
- package/dist/validation/{string.mjs → string.js} +35 -35
- package/dist/validation/type.js +34 -0
- package/dist/{z-schema-base.d.mts → z-schema-base.d.ts} +11 -12
- package/dist/{z-schema-base.mjs → z-schema-base.js} +45 -40
- package/dist/{z-schema-options.d.mts → z-schema-options.d.ts} +3 -3
- package/dist/{z-schema-options.mjs → z-schema-options.js} +5 -5
- package/dist/{z-schema-reader.d.mts → z-schema-reader.d.ts} +1 -1
- package/dist/{z-schema-versions.mjs → z-schema-versions.js} +21 -21
- package/dist/{z-schema.d.mts → z-schema.d.ts} +5 -13
- package/dist/{z-schema.mjs → z-schema.js} +38 -48
- package/package.json +24 -25
- package/src/errors.ts +1 -2
- package/src/format-validators.ts +173 -67
- package/src/json-schema-versions.ts +56 -2
- package/src/json-schema.ts +10 -9
- package/src/json-validation.ts +189 -146
- package/src/report.ts +37 -49
- package/src/schema-cache.ts +13 -13
- package/src/schema-compiler.ts +170 -117
- package/src/schema-validator.ts +239 -238
- package/src/utils/array.ts +9 -6
- package/src/utils/base64.ts +13 -2
- package/src/utils/clone.ts +28 -30
- package/src/utils/date.ts +6 -3
- package/src/utils/hostname.ts +257 -31
- package/src/utils/json.ts +16 -9
- package/src/utils/properties.ts +2 -2
- package/src/utils/schema-regex.ts +4 -4
- package/src/utils/time.ts +8 -7
- package/src/utils/unicode.ts +12 -5
- package/src/utils/what-is.ts +1 -5
- package/src/validation/array.ts +24 -22
- package/src/validation/combinators.ts +14 -14
- package/src/validation/numeric.ts +14 -28
- package/src/validation/object.ts +32 -36
- package/src/validation/ref.ts +5 -6
- package/src/validation/shared.ts +22 -21
- package/src/validation/string.ts +36 -42
- package/src/validation/type.ts +17 -17
- package/src/z-schema-base.ts +49 -38
- package/src/z-schema-options.ts +4 -3
- package/src/z-schema.ts +35 -45
- package/umd/ZSchema.js +853 -715
- package/umd/ZSchema.min.js +2 -2
- package/umd/package.json +3 -0
- package/dist/utils/hostname.mjs +0 -75
- package/dist/utils/unicode.mjs +0 -12
- package/dist/validation/type.mjs +0 -32
- /package/dist/{format-validators.d.mts → format-validators.d.ts} +0 -0
- /package/dist/{json-schema-versions.mjs → json-schema-versions.js} +0 -0
- /package/dist/schemas/{draft-04-schema.mjs → draft-04-schema.js} +0 -0
- /package/dist/schemas/{draft-06-schema.mjs → draft-06-schema.js} +0 -0
- /package/dist/schemas/{draft-07-schema.mjs → draft-07-schema.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-applicator.mjs → draft-2019-09-meta-applicator.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-content.mjs → draft-2019-09-meta-content.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-core.mjs → draft-2019-09-meta-core.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-format.mjs → draft-2019-09-meta-format.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-meta-data.mjs → draft-2019-09-meta-meta-data.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-validation.mjs → draft-2019-09-meta-validation.js} +0 -0
- /package/dist/schemas/{draft-2019-09-schema.mjs → draft-2019-09-schema.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-applicator.mjs → draft-2020-12-meta-applicator.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-content.mjs → draft-2020-12-meta-content.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-core.mjs → draft-2020-12-meta-core.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-format-annotation.mjs → draft-2020-12-meta-format-annotation.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-format-assertion.mjs → draft-2020-12-meta-format-assertion.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-meta-data.mjs → draft-2020-12-meta-meta-data.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-unevaluated.mjs → draft-2020-12-meta-unevaluated.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-validation.mjs → draft-2020-12-meta-validation.js} +0 -0
- /package/dist/schemas/{draft-2020-12-schema.mjs → draft-2020-12-schema.js} +0 -0
- /package/dist/utils/{constants.mjs → constants.js} +0 -0
- /package/dist/utils/{date.mjs → date.js} +0 -0
- /package/dist/utils/{properties.mjs → properties.js} +0 -0
- /package/dist/utils/{symbols.mjs → symbols.js} +0 -0
- /package/dist/utils/{uri.mjs → uri.js} +0 -0
- /package/dist/{z-schema-reader.mjs → z-schema-reader.js} +0 -0
package/src/utils/array.ts
CHANGED
|
@@ -7,9 +7,11 @@ import { areEqual } from './json.js';
|
|
|
7
7
|
* Falls back to pairwise deep comparison (O(n²)) when the array contains
|
|
8
8
|
* objects or arrays that need structural equality checks.
|
|
9
9
|
*/
|
|
10
|
-
export const isUniqueArray =
|
|
10
|
+
export const isUniqueArray = (arr: unknown[], indexes?: number[], maxDepth?: number): boolean => {
|
|
11
11
|
const l = arr.length;
|
|
12
|
-
if (l <= 1)
|
|
12
|
+
if (l <= 1) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
13
15
|
|
|
14
16
|
// Fast path: if every element is a primitive, use a Set.
|
|
15
17
|
// We distinguish types so that e.g. 1 !== '1' and 0 !== false.
|
|
@@ -27,7 +29,7 @@ export const isUniqueArray = <T>(arr: T[], indexes?: number[], maxDepth?: number
|
|
|
27
29
|
const seen = new Set<string>();
|
|
28
30
|
for (let i = 0; i < l; i++) {
|
|
29
31
|
const v = arr[i];
|
|
30
|
-
const key = typeof v
|
|
32
|
+
const key = `${typeof v}:${String(v)}`;
|
|
31
33
|
if (seen.has(key)) {
|
|
32
34
|
// Find the first occurrence for the indexes report.
|
|
33
35
|
if (indexes) {
|
|
@@ -47,9 +49,10 @@ export const isUniqueArray = <T>(arr: T[], indexes?: number[], maxDepth?: number
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// Slow path: at least one element is an object/array — need deep comparison.
|
|
52
|
+
const eqOpts = { maxDepth };
|
|
50
53
|
for (let i = 0; i < l; i++) {
|
|
51
54
|
for (let j = i + 1; j < l; j++) {
|
|
52
|
-
if (areEqual(arr[i], arr[j],
|
|
55
|
+
if (areEqual(arr[i], arr[j], eqOpts)) {
|
|
53
56
|
if (indexes) {
|
|
54
57
|
indexes.push(i, j);
|
|
55
58
|
}
|
|
@@ -60,9 +63,9 @@ export const isUniqueArray = <T>(arr: T[], indexes?: number[], maxDepth?: number
|
|
|
60
63
|
return true;
|
|
61
64
|
};
|
|
62
65
|
|
|
63
|
-
export const difference = (bigSet:
|
|
66
|
+
export const difference = <T>(bigSet: readonly T[], subSet: readonly T[]): T[] => {
|
|
64
67
|
const exclusions = new Set(subSet);
|
|
65
|
-
const arr = [];
|
|
68
|
+
const arr: T[] = [];
|
|
66
69
|
let idx = bigSet.length;
|
|
67
70
|
while (idx--) {
|
|
68
71
|
if (!exclusions.has(bigSet[idx])) {
|
package/src/utils/base64.ts
CHANGED
|
@@ -20,9 +20,20 @@ export const decodeBase64 = (value: string): string | undefined => {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Node fallback (reached only when `atob` is unavailable, i.e. not a browser).
|
|
24
|
+
// Read `Buffer` off `globalThis` rather than the bare global so it is typed
|
|
25
|
+
// without an untyped global reference; on Node >=22 `Buffer` is always present
|
|
26
|
+
// on `globalThis`. (A bundler polyfill injected as a module-scoped variable but
|
|
27
|
+
// not onto `globalThis` would be missed, but that path is unreachable in browsers
|
|
28
|
+
// where `atob` above is used instead.)
|
|
29
|
+
const bufferCtor = (
|
|
30
|
+
globalThis as {
|
|
31
|
+
Buffer?: { from: (data: string, encoding: string) => { toString: (encoding: string) => string } };
|
|
32
|
+
}
|
|
33
|
+
).Buffer;
|
|
34
|
+
if (bufferCtor !== undefined) {
|
|
24
35
|
try {
|
|
25
|
-
return
|
|
36
|
+
return bufferCtor.from(value, 'base64').toString('utf-8');
|
|
26
37
|
} catch {
|
|
27
38
|
return undefined;
|
|
28
39
|
}
|
package/src/utils/clone.ts
CHANGED
|
@@ -5,29 +5,28 @@ export const shallowClone = <T>(src: T): T => {
|
|
|
5
5
|
if (src == null || typeof src !== 'object') {
|
|
6
6
|
return src;
|
|
7
7
|
}
|
|
8
|
-
let res: any;
|
|
9
8
|
if (Array.isArray(src)) {
|
|
10
|
-
res = [];
|
|
9
|
+
const res: unknown[] = [];
|
|
11
10
|
for (let i = 0; i < src.length; i++) {
|
|
12
11
|
res[i] = src[i];
|
|
13
12
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
return res as T;
|
|
14
|
+
}
|
|
15
|
+
const res: Record<string, unknown> = {};
|
|
16
|
+
const keys = Object.keys(src).sort() as Array<keyof T>;
|
|
17
|
+
for (let i = 0; i < keys.length; i++) {
|
|
18
|
+
copyProp(src, res, keys[i]);
|
|
20
19
|
}
|
|
21
|
-
return res;
|
|
20
|
+
return res as T;
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
export const deepClone = <T>(src: T, maxDepth = DEFAULT_MAX_RECURSION_DEPTH): T => {
|
|
25
24
|
let vidx = 0;
|
|
26
|
-
const visited = new Map();
|
|
27
|
-
const cloned:
|
|
28
|
-
const cloneDeepInner = <
|
|
29
|
-
if (typeof
|
|
30
|
-
return
|
|
25
|
+
const visited = new Map<unknown, number>();
|
|
26
|
+
const cloned: unknown[] = [];
|
|
27
|
+
const cloneDeepInner = <U>(node: U, _depth: number): U => {
|
|
28
|
+
if (typeof node !== 'object' || node === null) {
|
|
29
|
+
return node;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
if (_depth >= maxDepth) {
|
|
@@ -37,29 +36,28 @@ export const deepClone = <T>(src: T, maxDepth = DEFAULT_MAX_RECURSION_DEPTH): T
|
|
|
37
36
|
);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
const cidx = visited.get(src);
|
|
39
|
+
const cidx = visited.get(node);
|
|
42
40
|
|
|
43
41
|
if (cidx !== undefined) {
|
|
44
|
-
return cloned[cidx];
|
|
42
|
+
return cloned[cidx] as U;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
visited.set(
|
|
48
|
-
if (Array.isArray(
|
|
49
|
-
res = [];
|
|
45
|
+
visited.set(node, vidx++);
|
|
46
|
+
if (Array.isArray(node)) {
|
|
47
|
+
const res: unknown[] = [];
|
|
50
48
|
cloned.push(res);
|
|
51
|
-
for (let i = 0; i <
|
|
52
|
-
res[i] = cloneDeepInner(
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
res = {};
|
|
56
|
-
cloned.push(res);
|
|
57
|
-
const keys = Object.keys(src).sort() as Array<keyof T>;
|
|
58
|
-
for (const key of keys) {
|
|
59
|
-
copyProp(src, res, key, (v: any) => cloneDeepInner(v, _depth + 1));
|
|
49
|
+
for (let i = 0; i < node.length; i++) {
|
|
50
|
+
res[i] = cloneDeepInner(node[i], _depth + 1);
|
|
60
51
|
}
|
|
52
|
+
return res as U;
|
|
53
|
+
}
|
|
54
|
+
const res: Record<string, unknown> = {};
|
|
55
|
+
cloned.push(res);
|
|
56
|
+
const keys = Object.keys(node).sort() as Array<keyof U>;
|
|
57
|
+
for (let i = 0; i < keys.length; i++) {
|
|
58
|
+
copyProp(node, res, keys[i], (v: unknown) => cloneDeepInner(v, _depth + 1));
|
|
61
59
|
}
|
|
62
|
-
return res;
|
|
60
|
+
return res as U;
|
|
63
61
|
};
|
|
64
62
|
return cloneDeepInner(src, 0);
|
|
65
63
|
};
|
package/src/utils/date.ts
CHANGED
|
@@ -2,15 +2,18 @@ const isLeapYear = (year: number): boolean => year % 4 === 0 && (year % 100 !==
|
|
|
2
2
|
|
|
3
3
|
const getDaysInMonth = (year: number, month: number): number => {
|
|
4
4
|
switch (month) {
|
|
5
|
-
case 2:
|
|
5
|
+
case 2: {
|
|
6
6
|
return isLeapYear(year) ? 29 : 28;
|
|
7
|
+
}
|
|
7
8
|
case 4:
|
|
8
9
|
case 6:
|
|
9
10
|
case 9:
|
|
10
|
-
case 11:
|
|
11
|
+
case 11: {
|
|
11
12
|
return 30;
|
|
12
|
-
|
|
13
|
+
}
|
|
14
|
+
default: {
|
|
13
15
|
return 31;
|
|
16
|
+
}
|
|
14
17
|
}
|
|
15
18
|
};
|
|
16
19
|
|
package/src/utils/hostname.ts
CHANGED
|
@@ -1,8 +1,188 @@
|
|
|
1
1
|
import punycode from 'punycode/punycode.js';
|
|
2
2
|
import isIPModule from 'validator/lib/isIP.js';
|
|
3
3
|
|
|
4
|
-
const IDN_SEPARATOR_REGEX = /[\u3002\
|
|
5
|
-
const IDN_SEPARATOR_TEST_REGEX = /[\u3002\
|
|
4
|
+
const IDN_SEPARATOR_REGEX = /[\u3002\uFF0E\uFF61]/g;
|
|
5
|
+
const IDN_SEPARATOR_TEST_REGEX = /[\u3002\uFF0E\uFF61]/;
|
|
6
|
+
|
|
7
|
+
// IDNA2008 (RFC 5890-5893) support below is a hand-rolled approximation built on
|
|
8
|
+
// General_Category / Script Unicode-property regexes (JS regex has no
|
|
9
|
+
// \p{Bidi_Class} or \p{Joining_Type}). It covers the JSON-Schema-Test-Suite
|
|
10
|
+
// corpus, not the full IDNA table. Known limitations: joining-context (CONTEXTJ)
|
|
11
|
+
// only models Arabic script; the ZWJ/ZWNJ Virama context recognizes only the
|
|
12
|
+
// Devanagari virama U+094D (not other scripts' viramas); Bidi class detection
|
|
13
|
+
// covers L/R/AL/AN/EN/NSM and treats everything else as ON; and U-labels are not
|
|
14
|
+
// required to be in Unicode NFC form. All regexes/Sets are hoisted to module scope
|
|
15
|
+
// so they are compiled once (format validation runs on hot paths).
|
|
16
|
+
|
|
17
|
+
// Matches an A-label (ACE) prefix; hoisted so it is compiled once, not per label.
|
|
18
|
+
const XN_LABEL_REGEX = /^xn--/i;
|
|
19
|
+
|
|
20
|
+
// RFC 5892 section 2.6 lists a handful of code points that are PVALID (or governed
|
|
21
|
+
// by a contextual rule) despite being Punctuation/Symbol. A blanket "reject
|
|
22
|
+
// P/S/Z/C" DISALLOWED heuristic must whitelist exactly these so valid labels are
|
|
23
|
+
// not rejected. Letters (e.g. U+00DF sharp s, U+03C2 final sigma) and Numbers
|
|
24
|
+
// (e.g. U+3007) are unaffected by the heuristic and need no entry here.
|
|
25
|
+
const DISALLOWED_CATEGORY_REGEX = /[\p{P}\p{S}\p{Z}\p{C}]/u;
|
|
26
|
+
const DISALLOWED_CATEGORY_WHITELIST: ReadonlySet<string> = new Set([
|
|
27
|
+
'-', // HYPHEN-MINUS (medial; leading/trailing rejected separately)
|
|
28
|
+
'\u00B7', // MIDDLE DOT (CONTEXTO)
|
|
29
|
+
'\u0375', // GREEK LOWER NUMERAL SIGN / KERAIA (CONTEXTO)
|
|
30
|
+
'\u05F3', // HEBREW PUNCTUATION GERESH (CONTEXTO)
|
|
31
|
+
'\u05F4', // HEBREW PUNCTUATION GERSHAYIM (CONTEXTO)
|
|
32
|
+
'\u30FB', // KATAKANA MIDDLE DOT (CONTEXTO)
|
|
33
|
+
'\u06FD', // ARABIC SIGN SINDHI AMPERSAND (RFC 5892 section 2.6 PVALID)
|
|
34
|
+
'\u06FE', // ARABIC SIGN SINDHI POSTPOSITION MEN (RFC 5892 section 2.6 PVALID)
|
|
35
|
+
'\u0F0B', // TIBETAN MARK INTERSYLLABIC TSHEG (RFC 5892 section 2.6 PVALID)
|
|
36
|
+
'\u200C', // ZERO WIDTH NON-JOINER (CONTEXTJ, checked separately)
|
|
37
|
+
'\u200D', // ZERO WIDTH JOINER (CONTEXTJ, checked separately)
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const isDisallowedCodePoint = (char: string): boolean =>
|
|
41
|
+
DISALLOWED_CATEGORY_REGEX.test(char) && !DISALLOWED_CATEGORY_WHITELIST.has(char);
|
|
42
|
+
|
|
43
|
+
// CONTEXTJ (RFC 5892 Appendix A.1) support for ZERO WIDTH NON-JOINER.
|
|
44
|
+
const TRANSPARENT_MARK_REGEX = /\p{Mn}/u;
|
|
45
|
+
const ARABIC_SCRIPT_REGEX = /\p{Script=Arabic}/u;
|
|
46
|
+
const ARABIC_INDIC_DIGITS_REGEX = /[\u0660-\u0669\u06F0-\u06F9]/;
|
|
47
|
+
|
|
48
|
+
// Approximates Joining_Type {L, D, R} as "an Arabic-script letter" (excluding the
|
|
49
|
+
// Arabic-Indic digit ranges, which are not letters).
|
|
50
|
+
const isArabicJoiningLetter = (char: string | undefined): boolean =>
|
|
51
|
+
char !== undefined && ARABIC_SCRIPT_REGEX.test(char) && !ARABIC_INDIC_DIGITS_REGEX.test(char);
|
|
52
|
+
|
|
53
|
+
// ZWNJ at `idx` is valid if it sits between two joining letters, skipping over any
|
|
54
|
+
// Transparent (combining-mark) code points on either side.
|
|
55
|
+
const hasZwnjJoiningContext = (label: string, idx: number): boolean => {
|
|
56
|
+
let before = idx - 1;
|
|
57
|
+
while (before >= 0 && TRANSPARENT_MARK_REGEX.test(label[before])) {
|
|
58
|
+
before--;
|
|
59
|
+
}
|
|
60
|
+
let after = idx + 1;
|
|
61
|
+
while (after < label.length && TRANSPARENT_MARK_REGEX.test(label[after])) {
|
|
62
|
+
after++;
|
|
63
|
+
}
|
|
64
|
+
return isArabicJoiningLetter(label[before]) && isArabicJoiningLetter(label[after]);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// RFC 5893 Bidi rule. Bidi_Class is approximated from Script / General_Category.
|
|
68
|
+
type BidiClass = 'AL' | 'AN' | 'EN' | 'L' | 'NSM' | 'ON' | 'R';
|
|
69
|
+
|
|
70
|
+
const BIDI_NSM_REGEX = /[\p{Mn}\p{Me}]/u;
|
|
71
|
+
const BIDI_ARABIC_INDIC_DIGIT_REGEX = /[\u0660-\u0669]/; // Bidi_Class AN
|
|
72
|
+
const BIDI_EXTENDED_ARABIC_INDIC_DIGIT_REGEX = /[\u06F0-\u06F9]/; // Bidi_Class EN (NOT AN)
|
|
73
|
+
const BIDI_HEBREW_SCRIPT_REGEX = /\p{Script=Hebrew}/u; // Bidi_Class R
|
|
74
|
+
const BIDI_AL_SCRIPT_REGEX = /[\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}]/u; // Bidi_Class AL
|
|
75
|
+
const BIDI_ASCII_DIGIT_REGEX = /[0-9]/; // Bidi_Class EN
|
|
76
|
+
const BIDI_LETTER_REGEX = /\p{L}/u;
|
|
77
|
+
|
|
78
|
+
const classifyBidi = (char: string): BidiClass => {
|
|
79
|
+
if (BIDI_NSM_REGEX.test(char)) {
|
|
80
|
+
return 'NSM';
|
|
81
|
+
}
|
|
82
|
+
if (BIDI_ARABIC_INDIC_DIGIT_REGEX.test(char)) {
|
|
83
|
+
return 'AN';
|
|
84
|
+
}
|
|
85
|
+
if (BIDI_EXTENDED_ARABIC_INDIC_DIGIT_REGEX.test(char)) {
|
|
86
|
+
return 'EN';
|
|
87
|
+
}
|
|
88
|
+
if (BIDI_HEBREW_SCRIPT_REGEX.test(char)) {
|
|
89
|
+
return 'R';
|
|
90
|
+
}
|
|
91
|
+
if (BIDI_AL_SCRIPT_REGEX.test(char)) {
|
|
92
|
+
return 'AL';
|
|
93
|
+
}
|
|
94
|
+
if (BIDI_ASCII_DIGIT_REGEX.test(char)) {
|
|
95
|
+
return 'EN';
|
|
96
|
+
}
|
|
97
|
+
if (BIDI_LETTER_REGEX.test(char)) {
|
|
98
|
+
return 'L';
|
|
99
|
+
}
|
|
100
|
+
return 'ON';
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// A domain is "Bidi" (and thus subject to the Bidi rule) if any label contains an
|
|
104
|
+
// R, AL, or AN character. Iterated by code point so surrogate pairs stay intact.
|
|
105
|
+
const isLabelBidiTriggering = (label: string): boolean => {
|
|
106
|
+
for (const char of label) {
|
|
107
|
+
const bidiClass = classifyBidi(char);
|
|
108
|
+
if (bidiClass === 'R' || bidiClass === 'AL' || bidiClass === 'AN') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const isLabelBidiRuleValid = (label: string): boolean => {
|
|
116
|
+
const classes: BidiClass[] = [];
|
|
117
|
+
for (const char of label) {
|
|
118
|
+
classes.push(classifyBidi(char));
|
|
119
|
+
}
|
|
120
|
+
if (classes.length === 0) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const first = classes[0];
|
|
125
|
+
// Rule 1: the first character must be L, R, or AL.
|
|
126
|
+
if (first !== 'L' && first !== 'R' && first !== 'AL') {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The "last" character check ignores trailing NSM marks.
|
|
131
|
+
let lastIdx = classes.length - 1;
|
|
132
|
+
while (lastIdx > 0 && classes[lastIdx] === 'NSM') {
|
|
133
|
+
lastIdx--;
|
|
134
|
+
}
|
|
135
|
+
const last = classes[lastIdx];
|
|
136
|
+
|
|
137
|
+
if (first === 'R' || first === 'AL') {
|
|
138
|
+
// Rule 2: RTL label.
|
|
139
|
+
let hasEN = false;
|
|
140
|
+
let hasAN = false;
|
|
141
|
+
for (let i = 0; i < classes.length; i++) {
|
|
142
|
+
const c = classes[i];
|
|
143
|
+
if (c !== 'R' && c !== 'AL' && c !== 'AN' && c !== 'EN' && c !== 'NSM' && c !== 'ON') {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (c === 'EN') {
|
|
147
|
+
hasEN = true;
|
|
148
|
+
} else if (c === 'AN') {
|
|
149
|
+
hasAN = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (hasEN && hasAN) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return last === 'R' || last === 'AL' || last === 'EN' || last === 'AN';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Rule 3: LTR label.
|
|
159
|
+
for (let i = 0; i < classes.length; i++) {
|
|
160
|
+
const c = classes[i];
|
|
161
|
+
if (c !== 'L' && c !== 'EN' && c !== 'NSM' && c !== 'ON') {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return last === 'L' || last === 'EN';
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const isDomainBidiValid = (unicodeLabels: string[]): boolean => {
|
|
169
|
+
let isBidiDomain = false;
|
|
170
|
+
for (let i = 0; i < unicodeLabels.length; i++) {
|
|
171
|
+
if (isLabelBidiTriggering(unicodeLabels[i])) {
|
|
172
|
+
isBidiDomain = true;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!isBidiDomain) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
for (let i = 0; i < unicodeLabels.length; i++) {
|
|
180
|
+
if (!isLabelBidiRuleValid(unicodeLabels[i])) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
};
|
|
6
186
|
|
|
7
187
|
const splitHostnameLabels = (hostname: string): string[] | null => {
|
|
8
188
|
if (hostname.length === 0 || hostname.length > 255) {
|
|
@@ -12,8 +192,11 @@ const splitHostnameLabels = (hostname: string): string[] | null => {
|
|
|
12
192
|
return null;
|
|
13
193
|
}
|
|
14
194
|
const labels = hostname.split('.');
|
|
15
|
-
|
|
16
|
-
|
|
195
|
+
for (let i = 0; i < labels.length; i++) {
|
|
196
|
+
const len = labels[i].length;
|
|
197
|
+
if (len === 0 || len > 63) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
17
200
|
}
|
|
18
201
|
return labels;
|
|
19
202
|
};
|
|
@@ -28,12 +211,12 @@ const hasCjkKanaOrHan = (input: string): boolean =>
|
|
|
28
211
|
/[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(input);
|
|
29
212
|
|
|
30
213
|
const toUnicodeLabel = (label: string): string | null => {
|
|
31
|
-
if (
|
|
214
|
+
if (!XN_LABEL_REGEX.test(label)) {
|
|
32
215
|
return label;
|
|
33
216
|
}
|
|
34
217
|
try {
|
|
35
218
|
return punycode.toUnicode(label.toLowerCase());
|
|
36
|
-
} catch
|
|
219
|
+
} catch {
|
|
37
220
|
return null;
|
|
38
221
|
}
|
|
39
222
|
};
|
|
@@ -43,7 +226,7 @@ const isValidIdnUnicodeLabel = (label: string): boolean => {
|
|
|
43
226
|
return false;
|
|
44
227
|
}
|
|
45
228
|
|
|
46
|
-
if (label.length >= 4 && label[2] === '-' && label[3] === '-' &&
|
|
229
|
+
if (label.length >= 4 && label[2] === '-' && label[3] === '-' && !XN_LABEL_REGEX.test(label)) {
|
|
47
230
|
return false;
|
|
48
231
|
}
|
|
49
232
|
|
|
@@ -51,44 +234,54 @@ const isValidIdnUnicodeLabel = (label: string): boolean => {
|
|
|
51
234
|
return false;
|
|
52
235
|
}
|
|
53
236
|
|
|
54
|
-
if (/[\
|
|
237
|
+
if (/[\u302E\u302F\u0640\u07FA]/u.test(label)) {
|
|
55
238
|
return false;
|
|
56
239
|
}
|
|
57
240
|
|
|
58
241
|
for (let idx = 0; idx < label.length; idx++) {
|
|
59
242
|
const char = label[idx];
|
|
60
243
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
244
|
+
if (
|
|
245
|
+
char === '\u00B7' &&
|
|
246
|
+
(idx === 0 || idx === label.length - 1 || label[idx - 1] !== 'l' || label[idx + 1] !== 'l')
|
|
247
|
+
) {
|
|
248
|
+
return false;
|
|
65
249
|
}
|
|
66
250
|
|
|
67
|
-
if (char === '\u0375') {
|
|
68
|
-
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
251
|
+
if (char === '\u0375' && (idx === label.length - 1 || !isGreek(label[idx + 1]))) {
|
|
252
|
+
return false;
|
|
71
253
|
}
|
|
72
254
|
|
|
73
|
-
if (char === '\
|
|
74
|
-
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
255
|
+
if ((char === '\u05F3' || char === '\u05F4') && (idx === 0 || !isHebrew(label[idx - 1]))) {
|
|
256
|
+
return false;
|
|
77
257
|
}
|
|
78
258
|
|
|
79
|
-
if (char === '\
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
259
|
+
if (char === '\u200D' && (idx === 0 || label[idx - 1] !== '\u094D')) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ZERO WIDTH NON-JOINER (CONTEXTJ, RFC 5892 Appendix A.1): valid only when
|
|
264
|
+
// preceded by a Virama, or when it sits in a joining context.
|
|
265
|
+
if (char === '\u200C' && (idx === 0 || (label[idx - 1] !== '\u094D' && !hasZwnjJoiningContext(label, idx)))) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Reject IDNA2008-DISALLOWED code points (Punctuation/Symbol/Separator/Other,
|
|
271
|
+
// minus the RFC 5892 section 2.6 / contextual exceptions). Iterated by code point so a
|
|
272
|
+
// surrogate pair is not mistaken for a lone \p{Cs} half.
|
|
273
|
+
for (const char of label) {
|
|
274
|
+
if (isDisallowedCodePoint(char)) {
|
|
275
|
+
return false;
|
|
83
276
|
}
|
|
84
277
|
}
|
|
85
278
|
|
|
86
|
-
if (label.includes('\
|
|
279
|
+
if (label.includes('\u30FB') && !hasCjkKanaOrHan(label.replaceAll('・', ''))) {
|
|
87
280
|
return false;
|
|
88
281
|
}
|
|
89
282
|
|
|
90
283
|
const hasArabicIndic = /[\u0660-\u0669]/.test(label);
|
|
91
|
-
const hasExtendedArabicIndic = /[\
|
|
284
|
+
const hasExtendedArabicIndic = /[\u06F0-\u06F9]/.test(label);
|
|
92
285
|
if (hasArabicIndic && hasExtendedArabicIndic) {
|
|
93
286
|
return false;
|
|
94
287
|
}
|
|
@@ -129,7 +322,7 @@ const isValidIdnUnicodeLabel = (label: string): boolean => {
|
|
|
129
322
|
*/
|
|
130
323
|
export const isValidHostname = (hostname: string): boolean => {
|
|
131
324
|
// eslint-disable-next-line no-control-regex
|
|
132
|
-
if (IDN_SEPARATOR_TEST_REGEX.test(hostname) || /[^\
|
|
325
|
+
if (IDN_SEPARATOR_TEST_REGEX.test(hostname) || /[^\u0000-\u007F]/.test(hostname)) {
|
|
133
326
|
return false;
|
|
134
327
|
}
|
|
135
328
|
|
|
@@ -142,13 +335,14 @@ export const isValidHostname = (hostname: string): boolean => {
|
|
|
142
335
|
return false;
|
|
143
336
|
}
|
|
144
337
|
|
|
145
|
-
for (
|
|
338
|
+
for (let i = 0; i < labels.length; i++) {
|
|
339
|
+
const label = labels[i];
|
|
146
340
|
if (!isAsciiHostnameLabel(label)) {
|
|
147
341
|
return false;
|
|
148
342
|
}
|
|
149
343
|
|
|
150
344
|
// Punycode (A-label) hostnames encode IDN labels, so validate the decoded Unicode form
|
|
151
|
-
if (
|
|
345
|
+
if (XN_LABEL_REGEX.test(label)) {
|
|
152
346
|
const unicodeLabel = toUnicodeLabel(label);
|
|
153
347
|
if (unicodeLabel === null || !isValidIdnUnicodeLabel(unicodeLabel)) {
|
|
154
348
|
return false;
|
|
@@ -166,12 +360,44 @@ export const isValidIdnHostname = (hostname: string): boolean => {
|
|
|
166
360
|
return false;
|
|
167
361
|
}
|
|
168
362
|
|
|
169
|
-
|
|
363
|
+
const unicodeLabels: string[] = [];
|
|
364
|
+
let totalLength = labels.length - 1; // separator dots between labels
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < labels.length; i++) {
|
|
367
|
+
const label = labels[i];
|
|
170
368
|
const unicodeLabel = toUnicodeLabel(label);
|
|
171
369
|
if (unicodeLabel === null || !isValidIdnUnicodeLabel(unicodeLabel)) {
|
|
172
370
|
return false;
|
|
173
371
|
}
|
|
372
|
+
|
|
373
|
+
let aLabel: string;
|
|
374
|
+
try {
|
|
375
|
+
aLabel = punycode.toASCII(unicodeLabel);
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// An A-label (xn-- input) must be canonical Punycode: re-encoding its decoded
|
|
381
|
+
// U-label must reproduce the original A-label. This also rejects an A-label
|
|
382
|
+
// that decodes to pure ASCII, since such a label never re-encodes to an "xn--"
|
|
383
|
+
// form (RFC 5890 §2.3.2.1, RFC 5891 §5.4).
|
|
384
|
+
if (XN_LABEL_REGEX.test(label) && aLabel.toLowerCase() !== label.toLowerCase()) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// RFC 5890 §2.3.2.1 — the A-label form of any label must not exceed 63 octets.
|
|
389
|
+
if (aLabel.length > 63) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
totalLength += aLabel.length;
|
|
394
|
+
unicodeLabels.push(unicodeLabel);
|
|
174
395
|
}
|
|
175
396
|
|
|
176
|
-
|
|
397
|
+
// RFC 1035 §3.1 — the domain name (in A-label form) must not exceed 253 octets.
|
|
398
|
+
if (totalLength > 253) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return isDomainBidiValid(unicodeLabels);
|
|
177
403
|
};
|
package/src/utils/json.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface AreEqualOptions {
|
|
|
6
6
|
maxDepth?: number;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export const sortedKeys = (obj: Record<string, unknown>): string[] => Object.keys(obj).sort();
|
|
10
|
+
|
|
9
11
|
export const areEqual = (json1: unknown, json2: unknown, options?: AreEqualOptions, _depth = 0): boolean => {
|
|
10
12
|
const caseInsensitiveComparison = options?.caseInsensitiveComparison || false;
|
|
11
13
|
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_RECURSION_DEPTH;
|
|
@@ -21,7 +23,7 @@ export const areEqual = (json1: unknown, json2: unknown, options?: AreEqualOptio
|
|
|
21
23
|
return true;
|
|
22
24
|
}
|
|
23
25
|
if (
|
|
24
|
-
caseInsensitiveComparison
|
|
26
|
+
caseInsensitiveComparison &&
|
|
25
27
|
typeof json1 === 'string' &&
|
|
26
28
|
typeof json2 === 'string' &&
|
|
27
29
|
json1.toUpperCase() === json2.toUpperCase()
|
|
@@ -75,16 +77,21 @@ export const areEqual = (json1: unknown, json2: unknown, options?: AreEqualOptio
|
|
|
75
77
|
return false;
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
export const decodeJSONPointer = (str: string) =>
|
|
79
|
-
|
|
80
|
-
return decodeURIComponent(str).replace(/~[0-1]/g, (x) => (x === '~1' ? '/' : '~'));
|
|
81
|
-
};
|
|
80
|
+
export const decodeJSONPointer = (str: string) =>
|
|
81
|
+
decodeURIComponent(str).replaceAll(/~[0-1]/g, (x) => (x === '~1' ? '/' : '~'));
|
|
82
82
|
|
|
83
|
-
export const
|
|
84
|
-
|
|
85
|
-
export const get = (obj: any, path: string | Array<string | number>): any => {
|
|
83
|
+
export const get = (obj: unknown, path: string | Array<string | number>): unknown => {
|
|
86
84
|
if (typeof path === 'string') {
|
|
87
85
|
path = path.split('.');
|
|
88
86
|
}
|
|
89
|
-
|
|
87
|
+
let acc: unknown = obj;
|
|
88
|
+
for (let i = 0; i < path.length; i++) {
|
|
89
|
+
const key = path[i];
|
|
90
|
+
if (acc && (acc as Record<string | number, unknown>)[key] !== undefined) {
|
|
91
|
+
acc = (acc as Record<string | number, unknown>)[key];
|
|
92
|
+
} else {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return acc;
|
|
90
97
|
};
|
package/src/utils/properties.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export function copyProp(from: object, to: object, key: string | number | symbol, fn?:
|
|
1
|
+
export function copyProp(from: object, to: object, key: string | number | symbol, fn?: (value: unknown) => unknown) {
|
|
2
2
|
if (Object.hasOwn(from, key)) {
|
|
3
3
|
Object.defineProperty(to, key, {
|
|
4
|
-
value: fn ? fn((from as
|
|
4
|
+
value: fn ? fn((from as Record<PropertyKey, unknown>)[key]) : (from as Record<PropertyKey, unknown>)[key],
|
|
5
5
|
enumerable: true,
|
|
6
6
|
writable: true,
|
|
7
7
|
configurable: true,
|
|
@@ -40,12 +40,12 @@ export function compileSchemaRegex(
|
|
|
40
40
|
try {
|
|
41
41
|
const re = new RegExp(pattern, 'u');
|
|
42
42
|
return { ok: true, value: re };
|
|
43
|
-
} catch (
|
|
43
|
+
} catch (error: unknown) {
|
|
44
44
|
return {
|
|
45
45
|
ok: false,
|
|
46
46
|
error: {
|
|
47
47
|
pattern,
|
|
48
|
-
message:
|
|
48
|
+
message: error instanceof Error ? error.message : 'Invalid regular expression',
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
51
|
}
|
|
@@ -53,12 +53,12 @@ export function compileSchemaRegex(
|
|
|
53
53
|
try {
|
|
54
54
|
const re = new RegExp(pattern);
|
|
55
55
|
return { ok: true, value: re };
|
|
56
|
-
} catch (
|
|
56
|
+
} catch (error: unknown) {
|
|
57
57
|
return {
|
|
58
58
|
ok: false,
|
|
59
59
|
error: {
|
|
60
60
|
pattern,
|
|
61
|
-
message:
|
|
61
|
+
message: error instanceof Error ? error.message : 'Invalid regular expression',
|
|
62
62
|
},
|
|
63
63
|
};
|
|
64
64
|
}
|