• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NaturalIntelligence / fast-xml-parser / 25647313727

11 May 2026 02:34AM UTC coverage: 97.61% (-0.06%) from 97.671%
25647313727

push

github

amitguptagwl
release info

1146 of 1193 branches covered (96.06%)

9517 of 9750 relevant lines covered (97.61%)

524886.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.48
/src/xmlparser/DocTypeReader.js
1
import { qName as isName } from 'xml-naming';
5✔
2

5✔
3
export default class DocTypeReader {
5✔
4
    constructor(options, xmlVersion) {
5✔
5
        this.suppressValidationErr = !options;
1,730✔
6
        this.options = options;
1,730✔
7
        this.xmlVersion = xmlVersion || 1.0;
1,730✔
8
    }
1,730✔
9

5✔
10
    setXmlVersion(xmlVersion = 1.0) {
5✔
11
        this.xmlVersion = xmlVersion;
250✔
12
    }
250✔
13
    readDocType(xmlData, i) {
5✔
14
        const entities = Object.create(null);
220✔
15
        let entityCount = 0;
220✔
16

220✔
17
        if (xmlData[i + 3] === 'O' &&
220✔
18
            xmlData[i + 4] === 'C' &&
220✔
19
            xmlData[i + 5] === 'T' &&
220✔
20
            xmlData[i + 6] === 'Y' &&
220✔
21
            xmlData[i + 7] === 'P' &&
220✔
22
            xmlData[i + 8] === 'E') {
220✔
23
            i = i + 9;
220✔
24
            let angleBracketsCount = 1;
220✔
25
            let hasBody = false, comment = false;
220✔
26
            let exp = "";
220✔
27
            for (; i < xmlData.length; i++) {
220✔
28
                if (xmlData[i] === '<' && !comment) { //Determine the tag type
5,885✔
29
                    if (hasBody && hasSeq(xmlData, "!ENTITY", i)) {
290✔
30
                        i += 7;
230✔
31
                        let entityName, val;
230✔
32
                        [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
230✔
33
                        if (val.indexOf("&") === -1) { //Parameter entities are not supported
230✔
34
                            if (this.options.enabled !== false &&
205✔
35
                                this.options.maxEntityCount != null &&
205✔
36
                                entityCount >= this.options.maxEntityCount) {
205!
37
                                throw new Error(
×
38
                                    `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`
×
39
                                );
×
40
                            }
×
41
                            //const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
205✔
42
                            //const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
205✔
43
                            entities[entityName] = val;
205✔
44
                            entityCount++;
205✔
45
                        }
205✔
46
                    }
230✔
47
                    else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
60✔
48
                        i += 8;//Not supported
20✔
49
                        const { index } = this.readElementExp(xmlData, i + 1);
20✔
50
                        i = index;
20✔
51
                    } else if (hasBody && hasSeq(xmlData, "!ATTLIST", i)) {
60✔
52
                        i += 8;//Not supported
5✔
53
                        // const {index} = this.readAttlistExp(xmlData,i+1);
5✔
54
                        // i = index;
5✔
55
                    } else if (hasBody && hasSeq(xmlData, "!NOTATION", i)) {
40✔
56
                        i += 9;//Not supported
10✔
57
                        const { index } = this.readNotationExp(xmlData, i + 1, this.suppressValidationErr);
10✔
58
                        i = index;
10✔
59
                    } else if (hasSeq(xmlData, "!--", i)) comment = true;
35✔
60
                    else throw new Error(`Invalid DOCTYPE`);
×
61

270✔
62
                    angleBracketsCount++;
270✔
63
                    exp = "";
270✔
64
                } else if (xmlData[i] === '>') { //Read tag content
5,885✔
65
                    if (comment) {
470✔
66
                        if (xmlData[i - 1] === "-" && xmlData[i - 2] === "-") {
30✔
67
                            comment = false;
25✔
68
                            angleBracketsCount--;
25✔
69
                        }
25✔
70
                    } else {
470✔
71
                        angleBracketsCount--;
440✔
72
                    }
440✔
73
                    if (angleBracketsCount === 0) {
470✔
74
                        break;
195✔
75
                    }
195✔
76
                } else if (xmlData[i] === '[') {
5,595✔
77
                    hasBody = true;
205✔
78
                } else {
5,125✔
79
                    exp += xmlData[i];
4,920✔
80
                }
4,920✔
81
            }
5,885✔
82
            if (angleBracketsCount !== 0) {
220✔
83
                throw new Error(`Unclosed DOCTYPE`);
5✔
84
            }
5✔
85
        } else {
220!
86
            throw new Error(`Invalid Tag instead of DOCTYPE`);
×
87
        }
×
88
        return { entities, i };
195✔
89
    }
220✔
90
    readEntityExp(xmlData, i) {
5✔
91
        //External entities are not supported
230✔
92
        //    <!ENTITY ext SYSTEM "http://normal-website.com" >
230✔
93

230✔
94
        //Parameter entities are not supported
230✔
95
        //    <!ENTITY entityname "&anotherElement;">
230✔
96

230✔
97
        //Internal entities are supported
230✔
98
        //    <!ENTITY entityname "replacement text">
230✔
99

230✔
100
        // Skip leading whitespace after <!ENTITY
230✔
101
        i = skipWhitespace(xmlData, i);
230✔
102

230✔
103
        // Read entity name
230✔
104
        const startIndex = i;
230✔
105
        while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
230✔
106
            i++;
805✔
107
        }
805✔
108
        let entityName = xmlData.substring(startIndex, i);
230✔
109

230✔
110
        validateEntityName(entityName, { xmlVersion: this.xmlVersion });
230✔
111

230✔
112
        // Skip whitespace after entity name
230✔
113
        i = skipWhitespace(xmlData, i);
230✔
114

230✔
115
        // Check for unsupported constructs (external entities or parameter entities)
230✔
116
        if (!this.suppressValidationErr) {
230✔
117
            if (xmlData.substring(i, i + 6).toUpperCase() === "SYSTEM") {
225!
118
                throw new Error("External entities are not supported");
×
119
            } else if (xmlData[i] === "%") {
225!
120
                throw new Error("Parameter entities are not supported");
×
121
            }
×
122
        }
225✔
123

225✔
124
        // Read entity value (internal entity)
225✔
125
        let entityValue = "";
225✔
126
        [i, entityValue] = this.readIdentifierVal(xmlData, i, "entity");
225✔
127

225✔
128
        // Validate entity size
225✔
129
        if (this.options.enabled !== false &&
230✔
130
            this.options.maxEntitySize != null &&
230✔
131
            entityValue.length > this.options.maxEntitySize) {
230✔
132
            throw new Error(
15✔
133
                `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
15✔
134
            );
15✔
135
        }
15✔
136

210✔
137
        i--;
210✔
138
        return [entityName, entityValue, i];
210✔
139
    }
230✔
140

5✔
141
    readNotationExp(xmlData, i) {
5✔
142
        // Skip leading whitespace after <!NOTATION
10✔
143
        i = skipWhitespace(xmlData, i);
10✔
144

10✔
145
        // Read notation name
10✔
146

10✔
147
        const startIndex = i;
10✔
148
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
10✔
149
            i++;
45✔
150
        }
45✔
151
        let notationName = xmlData.substring(startIndex, i);
10✔
152

10✔
153
        !this.suppressValidationErr && validateEntityName(notationName, { xmlVersion: this.xmlVersion });
10✔
154

10✔
155
        // Skip whitespace after notation name
10✔
156
        i = skipWhitespace(xmlData, i);
10✔
157

10✔
158
        // Check identifier type (SYSTEM or PUBLIC)
10✔
159
        const identifierType = xmlData.substring(i, i + 6).toUpperCase();
10✔
160
        if (!this.suppressValidationErr && identifierType !== "SYSTEM" && identifierType !== "PUBLIC") {
10!
161
            throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
×
162
        }
×
163
        i += identifierType.length;
10✔
164

10✔
165
        // Skip whitespace after identifier type
10✔
166
        i = skipWhitespace(xmlData, i);
10✔
167

10✔
168
        // Read public identifier (if PUBLIC)
10✔
169
        let publicIdentifier = null;
10✔
170
        let systemIdentifier = null;
10✔
171

10✔
172
        if (identifierType === "PUBLIC") {
10✔
173
            [i, publicIdentifier] = this.readIdentifierVal(xmlData, i, "publicIdentifier");
10✔
174

10✔
175
            // Skip whitespace after public identifier
10✔
176
            i = skipWhitespace(xmlData, i);
10✔
177

10✔
178
            // Optionally read system identifier
10✔
179
            if (xmlData[i] === '"' || xmlData[i] === "'") {
10✔
180
                [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
5✔
181
            }
5✔
182
        } else if (identifierType === "SYSTEM") {
10!
183
            // Read system identifier (mandatory for SYSTEM)
×
184
            [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
×
185

×
186
            if (!this.suppressValidationErr && !systemIdentifier) {
×
187
                throw new Error("Missing mandatory system identifier for SYSTEM notation");
×
188
            }
×
189
        }
×
190

10✔
191
        return { notationName, publicIdentifier, systemIdentifier, index: --i };
10✔
192
    }
10✔
193

5✔
194
    readIdentifierVal(xmlData, i, type) {
5✔
195
        let identifierVal = "";
240✔
196
        const startChar = xmlData[i];
240✔
197
        if (startChar !== '"' && startChar !== "'") {
240!
198
            throw new Error(`Expected quoted string, found "${startChar}"`);
×
199
        }
×
200
        i++;
240✔
201

240✔
202
        const startIndex = i;
240✔
203
        while (i < xmlData.length && xmlData[i] !== startChar) {
240✔
204
            i++;
441,490✔
205
        }
441,490✔
206
        identifierVal = xmlData.substring(startIndex, i);
240✔
207

240✔
208
        if (xmlData[i] !== startChar) {
240!
209
            throw new Error(`Unterminated ${type} value`);
×
210
        }
×
211
        i++;
240✔
212
        return [i, identifierVal];
240✔
213
    }
240✔
214

5✔
215
    readElementExp(xmlData, i) {
5✔
216
        // <!ELEMENT br EMPTY>
20✔
217
        // <!ELEMENT div ANY>
20✔
218
        // <!ELEMENT title (#PCDATA)>
20✔
219
        // <!ELEMENT book (title, author+)>
20✔
220
        // <!ELEMENT name (content-model)>
20✔
221

20✔
222
        // Skip leading whitespace after <!ELEMENT
20✔
223
        i = skipWhitespace(xmlData, i);
20✔
224

20✔
225
        // Read element name
20✔
226
        const startIndex = i;
20✔
227
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
20✔
228
            i++;
65✔
229
        }
65✔
230
        let elementName = xmlData.substring(startIndex, i);
20✔
231

20✔
232
        // Validate element name
20✔
233
        if (!this.suppressValidationErr && !isName(elementName, { xmlVersion: this.xmlVersion })) {
20!
234
            throw new Error(`Invalid element name: "${elementName}"`);
×
235
        }
×
236

20✔
237
        // Skip whitespace after element name
20✔
238
        i = skipWhitespace(xmlData, i);
20✔
239
        let contentModel = "";
20✔
240
        // Expect '(' to start content model
20✔
241
        if (xmlData[i] === "E" && hasSeq(xmlData, "MPTY", i)) i += 4;
20✔
242
        else if (xmlData[i] === "A" && hasSeq(xmlData, "NY", i)) i += 2;
15✔
243
        else if (xmlData[i] === "(") {
10✔
244
            i++; // Move past '('
10✔
245

10✔
246
            // Read content model
10✔
247
            const startIndex = i;
10✔
248
            while (i < xmlData.length && xmlData[i] !== ")") {
10✔
249
                i++;
70✔
250
            }
70✔
251
            contentModel = xmlData.substring(startIndex, i);
10✔
252

10✔
253
            if (xmlData[i] !== ")") {
10!
254
                throw new Error("Unterminated content model");
×
255
            }
×
256

10✔
257
        } else if (!this.suppressValidationErr) {
10!
258
            throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
×
259
        }
×
260

20✔
261
        return {
20✔
262
            elementName,
20✔
263
            contentModel: contentModel.trim(),
20✔
264
            index: i
20✔
265
        };
20✔
266
    }
20✔
267

5✔
268
    readAttlistExp(xmlData, i) {
5✔
269
        // Skip leading whitespace after <!ATTLIST
×
270
        i = skipWhitespace(xmlData, i);
×
271

×
272
        // Read element name
×
273
        let startIndex = i;
×
274
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
×
275
            i++;
×
276
        }
×
277
        let elementName = xmlData.substring(startIndex, i);
×
278

×
279
        // Validate element name
×
280
        validateEntityName(elementName, { xmlVersion: this.xmlVersion })
×
281

×
282
        // Skip whitespace after element name
×
283
        i = skipWhitespace(xmlData, i);
×
284

×
285
        // Read attribute name
×
286
        startIndex = i;
×
287
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
×
288
            i++;
×
289
        }
×
290
        let attributeName = xmlData.substring(startIndex, i);
×
291

×
292
        // Validate attribute name
×
293
        if (!validateEntityName(attributeName, { xmlVersion: this.xmlVersion })) {
×
294
            throw new Error(`Invalid attribute name: "${attributeName}"`);
×
295
        }
×
296

×
297
        // Skip whitespace after attribute name
×
298
        i = skipWhitespace(xmlData, i);
×
299

×
300
        // Read attribute type
×
301
        let attributeType = "";
×
302
        if (xmlData.substring(i, i + 8).toUpperCase() === "NOTATION") {
×
303
            attributeType = "NOTATION";
×
304
            i += 8; // Move past "NOTATION"
×
305

×
306
            // Skip whitespace after "NOTATION"
×
307
            i = skipWhitespace(xmlData, i);
×
308

×
309
            // Expect '(' to start the list of notations
×
310
            if (xmlData[i] !== "(") {
×
311
                throw new Error(`Expected '(', found "${xmlData[i]}"`);
×
312
            }
×
313
            i++; // Move past '('
×
314

×
315
            // Read the list of allowed notations
×
316
            let allowedNotations = [];
×
317
            while (i < xmlData.length && xmlData[i] !== ")") {
×
318

×
319

×
320
                const startIndex = i;
×
321
                while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
×
322
                    i++;
×
323
                }
×
324
                let notation = xmlData.substring(startIndex, i);
×
325

×
326
                // Validate notation name
×
327
                notation = notation.trim();
×
328
                if (!validateEntityName(notation, { xmlVersion: this.xmlVersion })) {
×
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
            const startIndex = i;
×
351
            while (i < xmlData.length && !/\s/.test(xmlData[i])) {
×
352
                i++;
×
353
            }
×
354
            attributeType += xmlData.substring(startIndex, i);
×
355

×
356
            // Validate simple attribute type
×
357
            const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
×
358
            if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
×
359
                throw new Error(`Invalid attribute type: "${attributeType}"`);
×
360
            }
×
361
        }
×
362

×
363
        // Skip whitespace after attribute type
×
364
        i = skipWhitespace(xmlData, i);
×
365

×
366
        // Read default value
×
367
        let defaultValue = "";
×
368
        if (xmlData.substring(i, i + 8).toUpperCase() === "#REQUIRED") {
×
369
            defaultValue = "#REQUIRED";
×
370
            i += 8;
×
371
        } else if (xmlData.substring(i, i + 7).toUpperCase() === "#IMPLIED") {
×
372
            defaultValue = "#IMPLIED";
×
373
            i += 7;
×
374
        } else {
×
375
            [i, defaultValue] = this.readIdentifierVal(xmlData, i, "ATTLIST");
×
376
        }
×
377

×
378
        return {
×
379
            elementName,
×
380
            attributeName,
×
381
            attributeType,
×
382
            defaultValue,
×
383
            index: i
×
384
        }
×
385
    }
×
386
}
5✔
387

5✔
388

5✔
389

5✔
390
const skipWhitespace = (data, index) => {
5✔
391
    while (index < data.length && /\s/.test(data[index])) {
535✔
392
        index++;
540✔
393
    }
540✔
394
    return index;
535✔
395
};
5✔
396

5✔
397

5✔
398

5✔
399
function hasSeq(data, seq, i) {
460✔
400
    for (let j = 0; j < seq.length; j++) {
460✔
401
        if (seq[j] !== data[i + j + 1]) return false;
2,345✔
402
    }
2,345✔
403
    return true;
300✔
404
}
460✔
405

5✔
406
function validateEntityName(name, xmlVersion) {
240✔
407
    if (isName(name, { xmlVersion: xmlVersion }))
240✔
408
        return name;
240✔
409
    else
5✔
410
        throw new Error(`Invalid entity name ${name}`);
5✔
411
}
240✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc