fast-xml-validator 1.4.0 → 1.4.2
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/lib/fxv.cjs +1 -1
- package/lib/fxv.d.cts +1 -1
- package/lib/fxv.min.js +1 -1
- package/lib/fxv.min.js.map +1 -1
- package/package.json +2 -2
- package/src/fxv.d.ts +1 -1
- package/src/validators/AttributeStringValidator.js +101 -4
- package/src/validators/DocTypeValidator.js +13 -1
- package/src/validators/XmlDeclarationValidator.js +97 -10
- package/src/DocTypeValidator.js +0 -280
- package/src/validator.js +0 -539
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-xml-validator",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Validate XML for Syntax and Business rules without C/C++ based libraries",
|
|
5
5
|
"main": "./lib/fxv.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -69,4 +69,4 @@
|
|
|
69
69
|
"path-expression-matcher": "^1.6.2",
|
|
70
70
|
"xml-naming": "^0.3.0"
|
|
71
71
|
}
|
|
72
|
-
}
|
|
72
|
+
}
|
package/src/fxv.d.ts
CHANGED
|
@@ -107,7 +107,7 @@ export type ValidationError = {
|
|
|
107
107
|
export class SyntaxValidator {
|
|
108
108
|
constructor(options?: validationOptions);
|
|
109
109
|
validate(xmlData: string): true;
|
|
110
|
-
static validate(xmlData: string, options?: validationOptions): true
|
|
110
|
+
static validate(xmlData: string, options?: validationOptions): true;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
|
|
@@ -1,9 +1,106 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { getPositionFromMatch, isWhiteSpace } from '../util.js';
|
|
4
4
|
import ControlCharValidator from './ControlCharValidator.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Walk `attrStr` once, left to right, splitting it into attribute tokens.
|
|
8
|
+
*
|
|
9
|
+
* This replaces a regex that used to do the same job
|
|
10
|
+
* (`(\s*)([^\s=]+)(\s*=)?(\s*(['"])(([\s\S])*?)\5)?`). That regex led with an
|
|
11
|
+
* optional whitespace group followed by a required "non-whitespace" group.
|
|
12
|
+
* On a long run of whitespace that never resolves into an attribute name
|
|
13
|
+
* (e.g. a tag with thousands of trailing spaces before `>`), the engine
|
|
14
|
+
* backtracks the whitespace group one character at a time before giving up
|
|
15
|
+
* and moving to the next starting position — one full backtrack per
|
|
16
|
+
* position, which is quadratic in the length of the run. A ~64KB tag made
|
|
17
|
+
* entirely of spaces could stall the whole process for seconds.
|
|
18
|
+
*
|
|
19
|
+
* A single forward-only scan can never backtrack, so it can't be made slow
|
|
20
|
+
* this way no matter how much whitespace the input contains — it's always
|
|
21
|
+
* proportional to the length of the string, once.
|
|
22
|
+
*
|
|
23
|
+
* Each returned token mirrors the shape the old regex match array had, so
|
|
24
|
+
* the validation logic below (which reads token[1]..token[6]) didn't need
|
|
25
|
+
* to change:
|
|
26
|
+
* token.startIndex - where this token begins in attrStr
|
|
27
|
+
* token[1] - leading whitespace before the name
|
|
28
|
+
* token[2] - the attribute name
|
|
29
|
+
* token[3] - whitespace + '=' if present, else undefined
|
|
30
|
+
* token[4] - marker (any defined value) if a quoted value was found
|
|
31
|
+
* token[5] - the quote character used ('"' or "'")
|
|
32
|
+
* token[6] - the value's text, without the surrounding quotes
|
|
33
|
+
*
|
|
34
|
+
* A malformed leading character (e.g. a stray '=' with no name before it)
|
|
35
|
+
* is simply skipped over, one character at a time — the same outcome the
|
|
36
|
+
* old regex produced by failing to match at that position and retrying at
|
|
37
|
+
* the next one.
|
|
38
|
+
*/
|
|
39
|
+
function scanAttributeTokens(attrStr) {
|
|
40
|
+
const tokens = [];
|
|
41
|
+
const len = attrStr.length;
|
|
42
|
+
let i = 0;
|
|
43
|
+
|
|
44
|
+
while (i < len) {
|
|
45
|
+
const tokenStart = i;
|
|
46
|
+
|
|
47
|
+
// Leading whitespace before the name.
|
|
48
|
+
while (i < len && isWhiteSpace(attrStr[i])) i++;
|
|
49
|
+
if (i >= len) break; // trailing whitespace only — nothing left to read
|
|
50
|
+
|
|
51
|
+
if (attrStr[i] === '=') {
|
|
52
|
+
// No name before this '=' — not a valid attribute start. Move past
|
|
53
|
+
// just this one character and try again from the next position.
|
|
54
|
+
i = tokenStart + 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const leadingWs = attrStr.slice(tokenStart, i);
|
|
59
|
+
|
|
60
|
+
// Attribute name — everything up to the next whitespace or '='.
|
|
61
|
+
const nameStart = i;
|
|
62
|
+
while (i < len && !isWhiteSpace(attrStr[i]) && attrStr[i] !== '=') i++;
|
|
63
|
+
const name = attrStr.slice(nameStart, i);
|
|
64
|
+
|
|
65
|
+
// Optional whitespace + '='.
|
|
66
|
+
let equalsGroup; // whitespace + '=' text, or undefined if absent
|
|
67
|
+
let j = i;
|
|
68
|
+
while (j < len && isWhiteSpace(attrStr[j])) j++;
|
|
69
|
+
if (j < len && attrStr[j] === '=') {
|
|
70
|
+
equalsGroup = attrStr.slice(i, j + 1);
|
|
71
|
+
i = j + 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Optional whitespace + quoted value.
|
|
75
|
+
let quoteChar;
|
|
76
|
+
let value;
|
|
77
|
+
let k = i;
|
|
78
|
+
while (k < len && isWhiteSpace(attrStr[k])) k++;
|
|
79
|
+
if (k < len && (attrStr[k] === '"' || attrStr[k] === "'")) {
|
|
80
|
+
const valueStart = k + 1;
|
|
81
|
+
const closeIdx = attrStr.indexOf(attrStr[k], valueStart);
|
|
82
|
+
if (closeIdx !== -1) {
|
|
83
|
+
quoteChar = attrStr[k];
|
|
84
|
+
value = attrStr.slice(valueStart, closeIdx);
|
|
85
|
+
i = closeIdx + 1;
|
|
86
|
+
}
|
|
87
|
+
// No closing quote found anywhere in the rest of the string — leave
|
|
88
|
+
// quoteChar/value undefined, same as the old regex's group failing
|
|
89
|
+
// to match a backreference-less run.
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const token = { startIndex: tokenStart };
|
|
93
|
+
token[1] = leadingWs;
|
|
94
|
+
token[2] = name;
|
|
95
|
+
token[3] = equalsGroup;
|
|
96
|
+
token[4] = quoteChar !== undefined ? true : undefined;
|
|
97
|
+
token[5] = quoteChar;
|
|
98
|
+
token[6] = value;
|
|
99
|
+
tokens.push(token);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return tokens;
|
|
103
|
+
}
|
|
7
104
|
|
|
8
105
|
/**
|
|
9
106
|
* AttributeStringValidator — single responsibility: validate the raw
|
|
@@ -40,7 +137,7 @@ export default class AttributeStringValidator {
|
|
|
40
137
|
* @returns {true | { err: { code: string, msg: string, line: number } }}
|
|
41
138
|
*/
|
|
42
139
|
validate(attrStr) {
|
|
43
|
-
const matches =
|
|
140
|
+
const matches = scanAttributeTokens(attrStr);
|
|
44
141
|
const attrNames = {};
|
|
45
142
|
const checkAttrLt = this.options.invalidCharSequence?.attrLt === true;
|
|
46
143
|
|
|
@@ -112,4 +209,4 @@ export default class AttributeStringValidator {
|
|
|
112
209
|
_err(code, msg, match) {
|
|
113
210
|
return { err: { code, msg, line: getPositionFromMatch(match) } };
|
|
114
211
|
}
|
|
115
|
-
}
|
|
212
|
+
}
|
|
@@ -28,9 +28,21 @@ export default class DocTypeValidator {
|
|
|
28
28
|
let angleBracketsCount = 1;
|
|
29
29
|
let hasBody = false;
|
|
30
30
|
let comment = false;
|
|
31
|
+
let quoteChar = null; // tracks an open SYSTEM/PUBLIC literal before the '[' body
|
|
31
32
|
let entityCount = 0;
|
|
32
33
|
|
|
33
34
|
for (; i < xmlData.length; i++) {
|
|
35
|
+
// Inside a quoted external-identifier literal — '<' and '>' are
|
|
36
|
+
// plain data here per the XML grammar, not DOCTYPE structure.
|
|
37
|
+
if (quoteChar !== null) {
|
|
38
|
+
if (xmlData[i] === quoteChar) quoteChar = null;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!hasBody && !comment && (xmlData[i] === '"' || xmlData[i] === "'")) {
|
|
42
|
+
quoteChar = xmlData[i];
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
if (xmlData[i] === '<' && !comment) {
|
|
35
47
|
if (hasBody && hasSeq(xmlData, '!ENTITY', i)) {
|
|
36
48
|
i += 7;
|
|
@@ -69,7 +81,7 @@ export default class DocTypeValidator {
|
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
if (angleBracketsCount !== 0) {
|
|
84
|
+
if (quoteChar !== null || angleBracketsCount !== 0) {
|
|
73
85
|
throw new Error(`Unclosed DOCTYPE`);
|
|
74
86
|
}
|
|
75
87
|
|
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import { isWhiteSpace } from '../util.js';
|
|
4
|
+
|
|
5
|
+
function isWordChar(ch) {
|
|
6
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch === '_';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Walk the `<?xml ... ?>` body once, left to right, pulling out
|
|
11
|
+
* `name="value"` / `name='value'` pairs (each one preceded by required
|
|
12
|
+
* whitespace) and collecting everything else as leftover text.
|
|
13
|
+
*
|
|
14
|
+
* This replaces two regexes that used to do the same job:
|
|
15
|
+
* /\s+([\w]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g - to find attributes
|
|
16
|
+
* /\s+[\w]+\s*=\s*(?:"[^"]*"|'[^']*')/g (via replace) - to find leftovers
|
|
17
|
+
*
|
|
18
|
+
* Both led with a required-but-greedy `\s+`. On a long run of whitespace
|
|
19
|
+
* that's never followed by a valid `name=value`, the engine backtracks that
|
|
20
|
+
* whitespace group one character at a time before giving up on that
|
|
21
|
+
* starting position — and since none of those backtracked attempts can ever
|
|
22
|
+
* succeed (a whitespace character is never a valid name character, at any
|
|
23
|
+
* backtrack length), all of that retrying was wasted work. A `<?xml ?>`
|
|
24
|
+
* declaration padded with thousands of spaces could make this step take
|
|
25
|
+
* seconds. Scanning forward once, without ever revisiting a position, is
|
|
26
|
+
* both faster and gives the identical result — the retried attempts could
|
|
27
|
+
* never have succeeded anyway.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} declBody
|
|
30
|
+
* @returns {{ matches: Array<{name: string, value: string, index: number}>, leftover: string }}
|
|
31
|
+
*/
|
|
32
|
+
function scanDeclAttributes(declBody) {
|
|
33
|
+
const matches = [];
|
|
34
|
+
let leftover = '';
|
|
35
|
+
const len = declBody.length;
|
|
36
|
+
let i = 0;
|
|
37
|
+
|
|
38
|
+
while (i < len) {
|
|
39
|
+
if (!isWhiteSpace(declBody[i])) {
|
|
40
|
+
leftover += declBody[i];
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const matchStart = i;
|
|
46
|
+
let p = i;
|
|
47
|
+
while (p < len && isWhiteSpace(declBody[p])) p++;
|
|
48
|
+
|
|
49
|
+
const nameStart = p;
|
|
50
|
+
while (p < len && isWordChar(declBody[p])) p++;
|
|
51
|
+
if (p === nameStart) {
|
|
52
|
+
// Whitespace not followed by a name character — can't become a match
|
|
53
|
+
// no matter how much of the whitespace we consider, so it's leftover.
|
|
54
|
+
leftover += declBody.slice(matchStart, p + 1 <= len ? p : len);
|
|
55
|
+
i = p > matchStart ? p : matchStart + 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const name = declBody.slice(nameStart, p);
|
|
59
|
+
|
|
60
|
+
let q = p;
|
|
61
|
+
while (q < len && isWhiteSpace(declBody[q])) q++;
|
|
62
|
+
if (declBody[q] !== '=') {
|
|
63
|
+
leftover += declBody.slice(matchStart, p);
|
|
64
|
+
i = p;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
q++; // skip '='
|
|
68
|
+
while (q < len && isWhiteSpace(declBody[q])) q++;
|
|
69
|
+
|
|
70
|
+
const quote = declBody[q];
|
|
71
|
+
if (quote !== '"' && quote !== "'") {
|
|
72
|
+
leftover += declBody.slice(matchStart, p);
|
|
73
|
+
i = p;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const valueStart = q + 1;
|
|
77
|
+
const closeIdx = declBody.indexOf(quote, valueStart);
|
|
78
|
+
if (closeIdx === -1) {
|
|
79
|
+
leftover += declBody.slice(matchStart, p);
|
|
80
|
+
i = p;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
matches.push({ name, value: declBody.slice(valueStart, closeIdx), index: matchStart });
|
|
85
|
+
i = closeIdx + 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { matches, leftover };
|
|
89
|
+
}
|
|
90
|
+
|
|
3
91
|
/**
|
|
4
92
|
* XmlDeclarationValidator — single responsibility: parse and validate the
|
|
5
93
|
* `<?xml version="..." encoding="..." standalone="..."?>` prolog.
|
|
@@ -49,16 +137,15 @@ export default class XmlDeclarationValidator {
|
|
|
49
137
|
const end = piEnd + 2;
|
|
50
138
|
|
|
51
139
|
const ALLOWED = ['version', 'encoding', 'standalone'];
|
|
52
|
-
const
|
|
140
|
+
const { matches: declAttrs, leftover } = scanDeclAttributes(declBody);
|
|
53
141
|
|
|
54
142
|
let version = '1.0';
|
|
55
143
|
let lastAllowedIdx = -1;
|
|
56
|
-
let match;
|
|
57
144
|
|
|
58
|
-
|
|
59
|
-
const attrName =
|
|
60
|
-
const attrValue =
|
|
61
|
-
const attrPos =
|
|
145
|
+
for (let m = 0; m < declAttrs.length; m++) {
|
|
146
|
+
const attrName = declAttrs[m].name;
|
|
147
|
+
const attrValue = declAttrs[m].value;
|
|
148
|
+
const attrPos = declAttrs[m].index;
|
|
62
149
|
|
|
63
150
|
const allowedIdx = ALLOWED.indexOf(attrName);
|
|
64
151
|
if (allowedIdx === -1) {
|
|
@@ -91,13 +178,13 @@ export default class XmlDeclarationValidator {
|
|
|
91
178
|
// encoding: value format validation is out of scope
|
|
92
179
|
}
|
|
93
180
|
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
181
|
+
const trimmedLeftover = leftover.trim();
|
|
182
|
+
if (trimmedLeftover.length > 0) {
|
|
96
183
|
this._throwError('InvalidXml',
|
|
97
|
-
'XML declaration contains invalid content: "' +
|
|
184
|
+
'XML declaration contains invalid content: "' + trimmedLeftover + '".',
|
|
98
185
|
{ line: 1, col: 1 });
|
|
99
186
|
}
|
|
100
187
|
|
|
101
188
|
return { version, end };
|
|
102
189
|
}
|
|
103
|
-
}
|
|
190
|
+
}
|
package/src/DocTypeValidator.js
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { name as isXmlName } from 'xml-naming';
|
|
2
|
-
|
|
3
|
-
const defaultOptions = {
|
|
4
|
-
maxEntityCount: Infinity,
|
|
5
|
-
maxEntitySize: Infinity,
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export default class DocTypeValidator {
|
|
9
|
-
constructor(options, xmlVersion = '1.0') {
|
|
10
|
-
this.options = Object.assign({}, defaultOptions, options);
|
|
11
|
-
this.suppressValidationErr = !options;
|
|
12
|
-
this.xmlVersion = xmlVersion;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
validateDocType(xmlData, i) {
|
|
16
|
-
if (
|
|
17
|
-
xmlData[i + 3] !== 'O' ||
|
|
18
|
-
xmlData[i + 4] !== 'C' ||
|
|
19
|
-
xmlData[i + 5] !== 'T' ||
|
|
20
|
-
xmlData[i + 6] !== 'Y' ||
|
|
21
|
-
xmlData[i + 7] !== 'P' ||
|
|
22
|
-
xmlData[i + 8] !== 'E'
|
|
23
|
-
) {
|
|
24
|
-
throw new Error(`Invalid Tag instead of DOCTYPE`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
i += 9;
|
|
28
|
-
let angleBracketsCount = 1;
|
|
29
|
-
let hasBody = false;
|
|
30
|
-
let comment = false;
|
|
31
|
-
let entityCount = 0;
|
|
32
|
-
|
|
33
|
-
for (; i < xmlData.length; i++) {
|
|
34
|
-
if (xmlData[i] === '<' && !comment) {
|
|
35
|
-
if (hasBody && hasSeq(xmlData, '!ENTITY', i)) {
|
|
36
|
-
i += 7;
|
|
37
|
-
[entityCount, i] = this._validateEntityExp(xmlData, i + 1, entityCount);
|
|
38
|
-
// continue;
|
|
39
|
-
} else if (hasBody && hasSeq(xmlData, '!ELEMENT', i)) {
|
|
40
|
-
i += 8;
|
|
41
|
-
i = this._validateElementExp(xmlData, i + 1);
|
|
42
|
-
// continue;
|
|
43
|
-
} else if (hasBody && hasSeq(xmlData, '!ATTLIST', i)) {
|
|
44
|
-
i += 8;
|
|
45
|
-
i = this._validateAttlistExp(xmlData, i + 1);
|
|
46
|
-
continue;
|
|
47
|
-
} else if (hasBody && hasSeq(xmlData, '!NOTATION', i)) {
|
|
48
|
-
i += 9;
|
|
49
|
-
i = this._validateNotationExp(xmlData, i + 1);
|
|
50
|
-
// continue;
|
|
51
|
-
} else if (hasSeq(xmlData, '!--', i)) {
|
|
52
|
-
comment = true;
|
|
53
|
-
} else {
|
|
54
|
-
throw new Error(`Invalid DOCTYPE`);
|
|
55
|
-
}
|
|
56
|
-
angleBracketsCount++;
|
|
57
|
-
} else if (xmlData[i] === '>') {
|
|
58
|
-
if (comment) {
|
|
59
|
-
if (xmlData[i - 1] === '-' && xmlData[i - 2] === '-') {
|
|
60
|
-
comment = false;
|
|
61
|
-
angleBracketsCount--;
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
angleBracketsCount--;
|
|
65
|
-
}
|
|
66
|
-
if (angleBracketsCount === 0) break;
|
|
67
|
-
} else if (xmlData[i] === '[') {
|
|
68
|
-
hasBody = true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (angleBracketsCount !== 0) {
|
|
73
|
-
throw new Error(`Unclosed DOCTYPE`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return i;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
_validateEntityExp(xmlData, i, entityCount) {
|
|
80
|
-
i = skipWhitespace(xmlData, i);
|
|
81
|
-
|
|
82
|
-
// Read and validate entity name
|
|
83
|
-
const nameStart = i;
|
|
84
|
-
while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
|
|
85
|
-
i++;
|
|
86
|
-
}
|
|
87
|
-
const entityName = xmlData.substring(nameStart, i);
|
|
88
|
-
validateName(entityName, this.xmlVersion);
|
|
89
|
-
|
|
90
|
-
i = skipWhitespace(xmlData, i);
|
|
91
|
-
|
|
92
|
-
// Check for unsupported constructs
|
|
93
|
-
if (!this.suppressValidationErr) {
|
|
94
|
-
if (xmlData.substring(i, i + 6).toUpperCase() === 'SYSTEM') {
|
|
95
|
-
throw new Error('External entities are not supported');
|
|
96
|
-
} else if (xmlData[i] === '%') {
|
|
97
|
-
throw new Error('Parameter entities are not supported');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Read and validate the entity value (validates quoting + size limit)
|
|
102
|
-
let entityValue;
|
|
103
|
-
[i, entityValue] = readQuotedValue(xmlData, i, 'entity');
|
|
104
|
-
|
|
105
|
-
const { maxEntitySize, maxEntityCount } = this.options;
|
|
106
|
-
|
|
107
|
-
if (entityValue.length > maxEntitySize) {
|
|
108
|
-
throw new Error(
|
|
109
|
-
`Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${maxEntitySize})`
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Only count non-parameter entities (parameter entities contain '&')
|
|
114
|
-
if (entityValue.indexOf('&') === -1) {
|
|
115
|
-
entityCount++;
|
|
116
|
-
if (entityCount > maxEntityCount) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`Entity count (${entityCount}) exceeds maximum allowed (${maxEntityCount})`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return [entityCount, i - 1];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
_validateNotationExp(xmlData, i) {
|
|
127
|
-
i = skipWhitespace(xmlData, i);
|
|
128
|
-
|
|
129
|
-
// Read and validate notation name
|
|
130
|
-
const nameStart = i;
|
|
131
|
-
while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
|
|
132
|
-
const notationName = xmlData.substring(nameStart, i);
|
|
133
|
-
!this.suppressValidationErr && validateName(notationName, this.xmlVersion);
|
|
134
|
-
|
|
135
|
-
i = skipWhitespace(xmlData, i);
|
|
136
|
-
|
|
137
|
-
const identifierType = xmlData.substring(i, i + 6).toUpperCase();
|
|
138
|
-
if (!this.suppressValidationErr && identifierType !== 'SYSTEM' && identifierType !== 'PUBLIC') {
|
|
139
|
-
throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
|
|
140
|
-
}
|
|
141
|
-
i += identifierType.length;
|
|
142
|
-
i = skipWhitespace(xmlData, i);
|
|
143
|
-
|
|
144
|
-
if (identifierType === 'PUBLIC') {
|
|
145
|
-
[i] = readQuotedValue(xmlData, i, 'publicIdentifier');
|
|
146
|
-
i = skipWhitespace(xmlData, i);
|
|
147
|
-
if (xmlData[i] === '"' || xmlData[i] === "'") {
|
|
148
|
-
[i] = readQuotedValue(xmlData, i, 'systemIdentifier');
|
|
149
|
-
}
|
|
150
|
-
} else if (identifierType === 'SYSTEM') {
|
|
151
|
-
[i] = readQuotedValue(xmlData, i, 'systemIdentifier');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return --i;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
_validateElementExp(xmlData, i) {
|
|
158
|
-
i = skipWhitespace(xmlData, i);
|
|
159
|
-
|
|
160
|
-
// Read and validate element name
|
|
161
|
-
const nameStart = i;
|
|
162
|
-
while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
|
|
163
|
-
const elementName = xmlData.substring(nameStart, i);
|
|
164
|
-
if (!this.suppressValidationErr && !isXmlName(elementName, { xmlVersion: this.xmlVersion })) {
|
|
165
|
-
throw new Error(`Invalid element name: "${elementName}"`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
i = skipWhitespace(xmlData, i);
|
|
169
|
-
|
|
170
|
-
// Validate content model keyword or group
|
|
171
|
-
if (xmlData[i] === 'E' && hasSeq(xmlData, 'MPTY', i)) {
|
|
172
|
-
i += 4;
|
|
173
|
-
} else if (xmlData[i] === 'A' && hasSeq(xmlData, 'NY', i)) {
|
|
174
|
-
i += 2;
|
|
175
|
-
} else if (xmlData[i] === '(') {
|
|
176
|
-
i++;
|
|
177
|
-
while (i < xmlData.length && xmlData[i] !== ')') i++;
|
|
178
|
-
if (xmlData[i] !== ')') throw new Error('Unterminated content model');
|
|
179
|
-
} else if (!this.suppressValidationErr) {
|
|
180
|
-
throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return i;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
_validateAttlistExp(xmlData, i) {
|
|
187
|
-
i = skipWhitespace(xmlData, i);
|
|
188
|
-
|
|
189
|
-
// Read element name
|
|
190
|
-
const elemNameStart = i;
|
|
191
|
-
while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
|
|
192
|
-
const elementName = xmlData.substring(elemNameStart, i);
|
|
193
|
-
validateName(elementName, this.xmlVersion);
|
|
194
|
-
|
|
195
|
-
i = skipWhitespace(xmlData, i);
|
|
196
|
-
|
|
197
|
-
// Read attribute name
|
|
198
|
-
const attrNameStart = i;
|
|
199
|
-
while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
|
|
200
|
-
const attributeName = xmlData.substring(attrNameStart, i);
|
|
201
|
-
validateName(attributeName, this.xmlVersion);
|
|
202
|
-
|
|
203
|
-
i = skipWhitespace(xmlData, i);
|
|
204
|
-
|
|
205
|
-
// Read attribute type
|
|
206
|
-
if (xmlData.substring(i, i + 8).toUpperCase() === 'NOTATION') {
|
|
207
|
-
i += 8;
|
|
208
|
-
i = skipWhitespace(xmlData, i);
|
|
209
|
-
if (xmlData[i] !== '(') throw new Error(`Expected '(', found "${xmlData[i]}"`);
|
|
210
|
-
i++;
|
|
211
|
-
while (i < xmlData.length && xmlData[i] !== ')') {
|
|
212
|
-
const notationStart = i;
|
|
213
|
-
while (i < xmlData.length && xmlData[i] !== '|' && xmlData[i] !== ')') i++;
|
|
214
|
-
const notation = xmlData.substring(notationStart, i).trim();
|
|
215
|
-
validateName(notation, this.xmlVersion);
|
|
216
|
-
if (xmlData[i] === '|') {
|
|
217
|
-
i++;
|
|
218
|
-
i = skipWhitespace(xmlData, i);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (xmlData[i] !== ')') throw new Error('Unterminated list of notations');
|
|
222
|
-
i++;
|
|
223
|
-
} else {
|
|
224
|
-
const typeStart = i;
|
|
225
|
-
while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
|
|
226
|
-
const attributeType = xmlData.substring(typeStart, i);
|
|
227
|
-
const validTypes = ['CDATA', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS'];
|
|
228
|
-
if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
|
|
229
|
-
throw new Error(`Invalid attribute type: "${attributeType}"`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
i = skipWhitespace(xmlData, i);
|
|
234
|
-
|
|
235
|
-
// Read default value declaration
|
|
236
|
-
if (xmlData.substring(i, i + 9).toUpperCase() === '#REQUIRED') {
|
|
237
|
-
i += 9;
|
|
238
|
-
} else if (xmlData.substring(i, i + 8).toUpperCase() === '#IMPLIED') {
|
|
239
|
-
i += 8;
|
|
240
|
-
} else {
|
|
241
|
-
[i] = readQuotedValue(xmlData, i, 'ATTLIST default value');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return i;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
function skipWhitespace(data, index) {
|
|
251
|
-
while (index < data.length && /\s/.test(data[index])) index++;
|
|
252
|
-
return index;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function hasSeq(data, seq, i) {
|
|
256
|
-
for (let j = 0; j < seq.length; j++) {
|
|
257
|
-
if (seq[j] !== data[i + j + 1]) return false;
|
|
258
|
-
}
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function validateName(name, xmlVersion = '1.0') {
|
|
263
|
-
if (!isXmlName(name, { xmlVersion })) throw new Error(`Invalid entity name "${name}"`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Read a single- or double-quoted string and return [nextIndex, value].
|
|
268
|
-
*/
|
|
269
|
-
function readQuotedValue(xmlData, i, type) {
|
|
270
|
-
const startChar = xmlData[i];
|
|
271
|
-
if (startChar !== '"' && startChar !== "'") {
|
|
272
|
-
throw new Error(`Expected quoted string for ${type}, found "${startChar}"`);
|
|
273
|
-
}
|
|
274
|
-
i++;
|
|
275
|
-
const start = i;
|
|
276
|
-
while (i < xmlData.length && xmlData[i] !== startChar) i++;
|
|
277
|
-
if (xmlData[i] !== startChar) throw new Error(`Unterminated ${type} value`);
|
|
278
|
-
const value = xmlData.substring(start, i);
|
|
279
|
-
return [++i, value];
|
|
280
|
-
}
|