fast-xml-validator 1.4.1 → 1.4.3
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 +57 -27
- package/lib/fxv.min.js +1 -1
- package/lib/fxv.min.js.map +1 -1
- package/package.json +4 -2
- package/src/SyntaxValidator.js +27 -4
- package/src/fxv.d.ts +46 -20
- 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/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
|
-
}
|