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.
@@ -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
- }