fast-xml-parser 4.5.3 → 4.5.5

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,153 +1,409 @@
1
1
  const util = require('../util');
2
2
 
3
- //TODO: handle comments
4
- function readDocType(xmlData, i){
5
-
6
- const entities = {};
7
- if( xmlData[i + 3] === 'O' &&
8
- xmlData[i + 4] === 'C' &&
9
- xmlData[i + 5] === 'T' &&
10
- xmlData[i + 6] === 'Y' &&
11
- xmlData[i + 7] === 'P' &&
12
- xmlData[i + 8] === 'E')
13
- {
14
- i = i+9;
15
- let angleBracketsCount = 1;
16
- let hasBody = false, comment = false;
17
- let exp = "";
18
- for(;i<xmlData.length;i++){
19
- if (xmlData[i] === '<' && !comment) { //Determine the tag type
20
- if( hasBody && isEntity(xmlData, i)){
21
- i += 7;
22
- let entityName, val;
23
- [entityName, val,i] = readEntityExp(xmlData,i+1);
24
- if(val.indexOf("&") === -1) //Parameter entities are not supported
25
- entities[ validateEntityName(entityName) ] = {
26
- regx : RegExp( `&${entityName};`,"g"),
27
- val: val
28
- };
29
- }
30
- else if( hasBody && isElement(xmlData, i)) i += 8;//Not supported
31
- else if( hasBody && isAttlist(xmlData, i)) i += 8;//Not supported
32
- else if( hasBody && isNotation(xmlData, i)) i += 9;//Not supported
33
- else if( isComment) comment = true;
34
- else throw new Error("Invalid DOCTYPE");
35
-
36
- angleBracketsCount++;
37
- exp = "";
38
- } else if (xmlData[i] === '>') { //Read tag content
39
- if(comment){
40
- if( xmlData[i - 1] === "-" && xmlData[i - 2] === "-"){
41
- comment = false;
3
+ class DocTypeReader {
4
+ constructor(options) {
5
+ this.suppressValidationErr = !options;
6
+ this.options = options || {};
7
+ }
8
+
9
+ readDocType(xmlData, i) {
10
+ const entities = Object.create(null);
11
+ let entityCount = 0;
12
+
13
+ if (xmlData[i + 3] === 'O' &&
14
+ xmlData[i + 4] === 'C' &&
15
+ xmlData[i + 5] === 'T' &&
16
+ xmlData[i + 6] === 'Y' &&
17
+ xmlData[i + 7] === 'P' &&
18
+ xmlData[i + 8] === 'E') {
19
+
20
+ i = i + 9;
21
+ let angleBracketsCount = 1;
22
+ let hasBody = false, comment = false;
23
+ let exp = "";
24
+
25
+ for (; i < xmlData.length; i++) {
26
+ if (xmlData[i] === '<' && !comment) { //Determine the tag type
27
+ if (hasBody && hasSeq(xmlData, "!ENTITY", i)) {
28
+ i += 7;
29
+ let entityName, val;
30
+ [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
31
+ if (val.indexOf("&") === -1) { //Parameter entities are not supported
32
+ if (this.options.enabled !== false &&
33
+ this.options.maxEntityCount != null &&
34
+ entityCount >= this.options.maxEntityCount) {
35
+ throw new Error(
36
+ `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`
37
+ );
38
+ }
39
+ //const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
40
+ const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ entities[entityName] = {
42
+ regx: RegExp(`&${escaped};`, "g"),
43
+ val: val
44
+ };
45
+ entityCount++;
46
+ }
47
+ } else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
48
+ i += 8; //Not supported
49
+ const { index } = this.readElementExp(xmlData, i + 1);
50
+ i = index;
51
+ } else if (hasBody && hasSeq(xmlData, "!ATTLIST", i)) {
52
+ i += 8; //Not supported
53
+ // const {index} = this.readAttlistExp(xmlData,i+1);
54
+ // i = index;
55
+ } else if (hasBody && hasSeq(xmlData, "!NOTATION", i)) {
56
+ i += 9; //Not supported
57
+ const { index } = this.readNotationExp(xmlData, i + 1, this.suppressValidationErr);
58
+ i = index;
59
+ } else if (hasSeq(xmlData, "!--", i)) {
60
+ comment = true;
61
+ } else {
62
+ throw new Error(`Invalid DOCTYPE`);
63
+ }
64
+
65
+ angleBracketsCount++;
66
+ exp = "";
67
+ } else if (xmlData[i] === '>') { //Read tag content
68
+ if (comment) {
69
+ if (xmlData[i - 1] === "-" && xmlData[i - 2] === "-") {
70
+ comment = false;
71
+ angleBracketsCount--;
72
+ }
73
+ } else {
42
74
  angleBracketsCount--;
43
75
  }
44
- }else{
45
- angleBracketsCount--;
46
- }
47
- if (angleBracketsCount === 0) {
48
- break;
76
+ if (angleBracketsCount === 0) {
77
+ break;
78
+ }
79
+ } else if (xmlData[i] === '[') {
80
+ hasBody = true;
81
+ } else {
82
+ exp += xmlData[i];
49
83
  }
50
- }else if( xmlData[i] === '['){
51
- hasBody = true;
52
- }else{
53
- exp += xmlData[i];
84
+ }
85
+
86
+ if (angleBracketsCount !== 0) {
87
+ throw new Error(`Unclosed DOCTYPE`);
88
+ }
89
+ } else {
90
+ throw new Error(`Invalid Tag instead of DOCTYPE`);
91
+ }
92
+
93
+ return { entities, i };
94
+ }
95
+
96
+ readEntityExp(xmlData, i) {
97
+ //External entities are not supported
98
+ // <!ENTITY ext SYSTEM "http://normal-website.com" >
99
+
100
+ //Parameter entities are not supported
101
+ // <!ENTITY entityname "&anotherElement;">
102
+
103
+ //Internal entities are supported
104
+ // <!ENTITY entityname "replacement text">
105
+
106
+ // Skip leading whitespace after <!ENTITY
107
+ i = skipWhitespace(xmlData, i);
108
+
109
+ // Read entity name
110
+ let entityName = "";
111
+ while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
112
+ entityName += xmlData[i];
113
+ i++;
114
+ }
115
+ validateEntityName(entityName);
116
+
117
+ // Skip whitespace after entity name
118
+ i = skipWhitespace(xmlData, i);
119
+
120
+ // Check for unsupported constructs (external entities or parameter entities)
121
+ if (!this.suppressValidationErr) {
122
+ if (xmlData.substring(i, i + 6).toUpperCase() === "SYSTEM") {
123
+ throw new Error("External entities are not supported");
124
+ } else if (xmlData[i] === "%") {
125
+ throw new Error("Parameter entities are not supported");
54
126
  }
55
127
  }
56
- if(angleBracketsCount !== 0){
57
- throw new Error(`Unclosed DOCTYPE`);
128
+
129
+ // Read entity value (internal entity)
130
+ let entityValue = "";
131
+ [i, entityValue] = this.readIdentifierVal(xmlData, i, "entity");
132
+
133
+ // Validate entity size
134
+ if (this.options.enabled !== false &&
135
+ this.options.maxEntitySize != null &&
136
+ entityValue.length > this.options.maxEntitySize) {
137
+ throw new Error(
138
+ `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
139
+ );
58
140
  }
59
- }else{
60
- throw new Error(`Invalid Tag instead of DOCTYPE`);
141
+
142
+ i--;
143
+ return [entityName, entityValue, i];
61
144
  }
62
- return {entities, i};
63
- }
64
145
 
65
- function readEntityExp(xmlData,i){
66
- //External entities are not supported
67
- // <!ENTITY ext SYSTEM "http://normal-website.com" >
68
-
69
- //Parameter entities are not supported
70
- // <!ENTITY entityname "&anotherElement;">
71
-
72
- //Internal entities are supported
73
- // <!ENTITY entityname "replacement text">
74
-
75
- //read EntityName
76
- let entityName = "";
77
- for (; i < xmlData.length && (xmlData[i] !== "'" && xmlData[i] !== '"' ); i++) {
78
- // if(xmlData[i] === " ") continue;
79
- // else
80
- entityName += xmlData[i];
146
+ readNotationExp(xmlData, i) {
147
+ // Skip leading whitespace after <!NOTATION
148
+ i = skipWhitespace(xmlData, i);
149
+
150
+ // Read notation name
151
+ let notationName = "";
152
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
153
+ notationName += xmlData[i];
154
+ i++;
155
+ }
156
+ !this.suppressValidationErr && validateEntityName(notationName);
157
+
158
+ // Skip whitespace after notation name
159
+ i = skipWhitespace(xmlData, i);
160
+
161
+ // Check identifier type (SYSTEM or PUBLIC)
162
+ const identifierType = xmlData.substring(i, i + 6).toUpperCase();
163
+ if (!this.suppressValidationErr && identifierType !== "SYSTEM" && identifierType !== "PUBLIC") {
164
+ throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
165
+ }
166
+ i += identifierType.length;
167
+
168
+ // Skip whitespace after identifier type
169
+ i = skipWhitespace(xmlData, i);
170
+
171
+ // Read public identifier (if PUBLIC)
172
+ let publicIdentifier = null;
173
+ let systemIdentifier = null;
174
+
175
+ if (identifierType === "PUBLIC") {
176
+ [i, publicIdentifier] = this.readIdentifierVal(xmlData, i, "publicIdentifier");
177
+
178
+ // Skip whitespace after public identifier
179
+ i = skipWhitespace(xmlData, i);
180
+
181
+ // Optionally read system identifier
182
+ if (xmlData[i] === '"' || xmlData[i] === "'") {
183
+ [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
184
+ }
185
+ } else if (identifierType === "SYSTEM") {
186
+ // Read system identifier (mandatory for SYSTEM)
187
+ [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
188
+
189
+ if (!this.suppressValidationErr && !systemIdentifier) {
190
+ throw new Error("Missing mandatory system identifier for SYSTEM notation");
191
+ }
192
+ }
193
+
194
+ return { notationName, publicIdentifier, systemIdentifier, index: --i };
81
195
  }
82
- entityName = entityName.trim();
83
- if(entityName.indexOf(" ") !== -1) throw new Error("External entites are not supported");
84
-
85
- //read Entity Value
86
- const startChar = xmlData[i++];
87
- let val = ""
88
- for (; i < xmlData.length && xmlData[i] !== startChar ; i++) {
89
- val += xmlData[i];
196
+
197
+ readIdentifierVal(xmlData, i, type) {
198
+ let identifierVal = "";
199
+ const startChar = xmlData[i];
200
+ if (startChar !== '"' && startChar !== "'") {
201
+ throw new Error(`Expected quoted string, found "${startChar}"`);
202
+ }
203
+ i++;
204
+
205
+ while (i < xmlData.length && xmlData[i] !== startChar) {
206
+ identifierVal += xmlData[i];
207
+ i++;
208
+ }
209
+
210
+ if (xmlData[i] !== startChar) {
211
+ throw new Error(`Unterminated ${type} value`);
212
+ }
213
+ i++;
214
+ return [i, identifierVal];
90
215
  }
91
- return [entityName, val, i];
92
- }
93
216
 
94
- function isComment(xmlData, i){
95
- if(xmlData[i+1] === '!' &&
96
- xmlData[i+2] === '-' &&
97
- xmlData[i+3] === '-') return true
98
- return false
99
- }
100
- function isEntity(xmlData, i){
101
- if(xmlData[i+1] === '!' &&
102
- xmlData[i+2] === 'E' &&
103
- xmlData[i+3] === 'N' &&
104
- xmlData[i+4] === 'T' &&
105
- xmlData[i+5] === 'I' &&
106
- xmlData[i+6] === 'T' &&
107
- xmlData[i+7] === 'Y') return true
108
- return false
109
- }
110
- function isElement(xmlData, i){
111
- if(xmlData[i+1] === '!' &&
112
- xmlData[i+2] === 'E' &&
113
- xmlData[i+3] === 'L' &&
114
- xmlData[i+4] === 'E' &&
115
- xmlData[i+5] === 'M' &&
116
- xmlData[i+6] === 'E' &&
117
- xmlData[i+7] === 'N' &&
118
- xmlData[i+8] === 'T') return true
119
- return false
120
- }
217
+ readElementExp(xmlData, i) {
218
+ // <!ELEMENT br EMPTY>
219
+ // <!ELEMENT div ANY>
220
+ // <!ELEMENT title (#PCDATA)>
221
+ // <!ELEMENT book (title, author+)>
222
+ // <!ELEMENT name (content-model)>
223
+
224
+ // Skip leading whitespace after <!ELEMENT
225
+ i = skipWhitespace(xmlData, i);
226
+
227
+ // Read element name
228
+ let elementName = "";
229
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
230
+ elementName += xmlData[i];
231
+ i++;
232
+ }
233
+
234
+ // Validate element name
235
+ if (!this.suppressValidationErr && !util.isName(elementName)) {
236
+ throw new Error(`Invalid element name: "${elementName}"`);
237
+ }
238
+
239
+ // Skip whitespace after element name
240
+ i = skipWhitespace(xmlData, i);
241
+ let contentModel = "";
242
+
243
+ // Expect '(' to start content model
244
+ if (xmlData[i] === "E" && hasSeq(xmlData, "MPTY", i)) {
245
+ i += 4;
246
+ } else if (xmlData[i] === "A" && hasSeq(xmlData, "NY", i)) {
247
+ i += 2;
248
+ } else if (xmlData[i] === "(") {
249
+ i++; // Move past '('
250
+
251
+ // Read content model
252
+ while (i < xmlData.length && xmlData[i] !== ")") {
253
+ contentModel += xmlData[i];
254
+ i++;
255
+ }
256
+ if (xmlData[i] !== ")") {
257
+ throw new Error("Unterminated content model");
258
+ }
259
+ } else if (!this.suppressValidationErr) {
260
+ throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
261
+ }
262
+
263
+ return {
264
+ elementName,
265
+ contentModel: contentModel.trim(),
266
+ index: i
267
+ };
268
+ }
269
+
270
+ readAttlistExp(xmlData, i) {
271
+ // Skip leading whitespace after <!ATTLIST
272
+ i = skipWhitespace(xmlData, i);
273
+
274
+ // Read element name
275
+ let elementName = "";
276
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
277
+ elementName += xmlData[i];
278
+ i++;
279
+ }
280
+
281
+ // Validate element name
282
+ validateEntityName(elementName);
283
+
284
+ // Skip whitespace after element name
285
+ i = skipWhitespace(xmlData, i);
286
+
287
+ // Read attribute name
288
+ let attributeName = "";
289
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
290
+ attributeName += xmlData[i];
291
+ i++;
292
+ }
293
+
294
+ // Validate attribute name
295
+ if (!validateEntityName(attributeName)) {
296
+ throw new Error(`Invalid attribute name: "${attributeName}"`);
297
+ }
298
+
299
+ // Skip whitespace after attribute name
300
+ i = skipWhitespace(xmlData, i);
301
+
302
+ // Read attribute type
303
+ let attributeType = "";
304
+ if (xmlData.substring(i, i + 8).toUpperCase() === "NOTATION") {
305
+ attributeType = "NOTATION";
306
+ i += 8; // Move past "NOTATION"
307
+
308
+ // Skip whitespace after "NOTATION"
309
+ i = skipWhitespace(xmlData, i);
121
310
 
122
- function isAttlist(xmlData, i){
123
- if(xmlData[i+1] === '!' &&
124
- xmlData[i+2] === 'A' &&
125
- xmlData[i+3] === 'T' &&
126
- xmlData[i+4] === 'T' &&
127
- xmlData[i+5] === 'L' &&
128
- xmlData[i+6] === 'I' &&
129
- xmlData[i+7] === 'S' &&
130
- xmlData[i+8] === 'T') return true
131
- return false
311
+ // Expect '(' to start the list of notations
312
+ if (xmlData[i] !== "(") {
313
+ throw new Error(`Expected '(', found "${xmlData[i]}"`);
314
+ }
315
+ i++; // Move past '('
316
+
317
+ // Read the list of allowed notations
318
+ let allowedNotations = [];
319
+ while (i < xmlData.length && xmlData[i] !== ")") {
320
+ let notation = "";
321
+ while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
322
+ notation += xmlData[i];
323
+ i++;
324
+ }
325
+
326
+ // Validate notation name
327
+ notation = notation.trim();
328
+ if (!validateEntityName(notation)) {
329
+ throw new Error(`Invalid notation name: "${notation}"`);
330
+ }
331
+
332
+ allowedNotations.push(notation);
333
+
334
+ // Skip '|' separator or exit loop
335
+ if (xmlData[i] === "|") {
336
+ i++; // Move past '|'
337
+ i = skipWhitespace(xmlData, i); // Skip optional whitespace after '|'
338
+ }
339
+ }
340
+
341
+ if (xmlData[i] !== ")") {
342
+ throw new Error("Unterminated list of notations");
343
+ }
344
+ i++; // Move past ')'
345
+
346
+ // Store the allowed notations as part of the attribute type
347
+ attributeType += " (" + allowedNotations.join("|") + ")";
348
+ } else {
349
+ // Handle simple types (e.g., CDATA, ID, IDREF, etc.)
350
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
351
+ attributeType += xmlData[i];
352
+ i++;
353
+ }
354
+
355
+ // Validate simple attribute type
356
+ const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
357
+ if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
358
+ throw new Error(`Invalid attribute type: "${attributeType}"`);
359
+ }
360
+ }
361
+
362
+ // Skip whitespace after attribute type
363
+ i = skipWhitespace(xmlData, i);
364
+
365
+ // Read default value
366
+ let defaultValue = "";
367
+ if (xmlData.substring(i, i + 8).toUpperCase() === "#REQUIRED") {
368
+ defaultValue = "#REQUIRED";
369
+ i += 8;
370
+ } else if (xmlData.substring(i, i + 7).toUpperCase() === "#IMPLIED") {
371
+ defaultValue = "#IMPLIED";
372
+ i += 7;
373
+ } else {
374
+ [i, defaultValue] = this.readIdentifierVal(xmlData, i, "ATTLIST");
375
+ }
376
+
377
+ return {
378
+ elementName,
379
+ attributeName,
380
+ attributeType,
381
+ defaultValue,
382
+ index: i
383
+ };
384
+ }
132
385
  }
133
- function isNotation(xmlData, i){
134
- if(xmlData[i+1] === '!' &&
135
- xmlData[i+2] === 'N' &&
136
- xmlData[i+3] === 'O' &&
137
- xmlData[i+4] === 'T' &&
138
- xmlData[i+5] === 'A' &&
139
- xmlData[i+6] === 'T' &&
140
- xmlData[i+7] === 'I' &&
141
- xmlData[i+8] === 'O' &&
142
- xmlData[i+9] === 'N') return true
143
- return false
386
+
387
+ // Helper functions
388
+ const skipWhitespace = (data, index) => {
389
+ while (index < data.length && /\s/.test(data[index])) {
390
+ index++;
391
+ }
392
+ return index;
393
+ };
394
+
395
+ function hasSeq(data, seq, i) {
396
+ for (let j = 0; j < seq.length; j++) {
397
+ if (seq[j] !== data[i + j + 1]) return false;
398
+ }
399
+ return true;
144
400
  }
145
401
 
146
- function validateEntityName(name){
402
+ function validateEntityName(name) {
147
403
  if (util.isName(name))
148
- return name;
404
+ return name;
149
405
  else
150
406
  throw new Error(`Invalid entity name ${name}`);
151
407
  }
152
408
 
153
- module.exports = readDocType;
409
+ module.exports = DocTypeReader;