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

NaturalIntelligence / fast-xml-parser / 22012305802

14 Feb 2026 05:53AM UTC coverage: 97.755% (+0.1%) from 97.617%
22012305802

push

github

amitguptagwl
update release info

1275 of 1319 branches covered (96.66%)

9753 of 9977 relevant lines covered (97.75%)

447181.27 hits per line

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

64.45
/src/xmlparser/DocTypeReader.js
1
import { isName } from '../util.js';
5✔
2

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

5✔
9
    readDocType(xmlData, i) {
5✔
10

220✔
11
        const entities = {};
220✔
12
        if (xmlData[i + 3] === 'O' &&
220✔
13
            xmlData[i + 4] === 'C' &&
220✔
14
            xmlData[i + 5] === 'T' &&
220✔
15
            xmlData[i + 6] === 'Y' &&
220✔
16
            xmlData[i + 7] === 'P' &&
220✔
17
            xmlData[i + 8] === 'E') {
220✔
18
            i = i + 9;
220✔
19
            let angleBracketsCount = 1;
220✔
20
            let hasBody = false, comment = false;
220✔
21
            let exp = "";
220✔
22
            for (; i < xmlData.length; i++) {
220✔
23
                if (xmlData[i] === '<' && !comment) { //Determine the tag type
5,885✔
24
                    if (hasBody && hasSeq(xmlData, "!ENTITY", i)) {
290✔
25
                        i += 7;
230✔
26
                        let entityName, val;
230✔
27
                        [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
230✔
28
                        if (val.indexOf("&") === -1) { //Parameter entities are not supported
230✔
29
                            const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
205✔
30
                            entities[entityName] = {
205✔
31
                                regx: RegExp(`&${escaped};`, "g"),
205✔
32
                                val: val
205✔
33
                            };
205✔
34
                        }
205✔
35
                    }
230✔
36
                    else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
60✔
37
                        i += 8;//Not supported
20✔
38
                        const { index } = this.readElementExp(xmlData, i + 1);
20✔
39
                        i = index;
20✔
40
                    } else if (hasBody && hasSeq(xmlData, "!ATTLIST", i)) {
60✔
41
                        i += 8;//Not supported
5✔
42
                        // const {index} = this.readAttlistExp(xmlData,i+1);
5✔
43
                        // i = index;
5✔
44
                    } else if (hasBody && hasSeq(xmlData, "!NOTATION", i)) {
40✔
45
                        i += 9;//Not supported
10✔
46
                        const { index } = this.readNotationExp(xmlData, i + 1, this.suppressValidationErr);
10✔
47
                        i = index;
10✔
48
                    } else if (hasSeq(xmlData, "!--", i)) comment = true;
35✔
49
                    else throw new Error(`Invalid DOCTYPE`);
×
50

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

230✔
83
        //Parameter entities are not supported
230✔
84
        //    <!ENTITY entityname "&anotherElement;">
230✔
85

230✔
86
        //Internal entities are supported
230✔
87
        //    <!ENTITY entityname "replacement text">
230✔
88

230✔
89
        // Skip leading whitespace after <!ENTITY
230✔
90
        i = skipWhitespace(xmlData, i);
230✔
91

230✔
92
        // Read entity name
230✔
93
        let entityName = "";
230✔
94
        while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
230✔
95
            entityName += xmlData[i];
805✔
96
            i++;
805✔
97
        }
805✔
98
        validateEntityName(entityName);
230✔
99

230✔
100
        // Skip whitespace after entity name
230✔
101
        i = skipWhitespace(xmlData, i);
230✔
102

230✔
103
        // Check for unsupported constructs (external entities or parameter entities)
230✔
104
        if (!this.suppressValidationErr) {
230✔
105
            if (xmlData.substring(i, i + 6).toUpperCase() === "SYSTEM") {
225!
106
                throw new Error("External entities are not supported");
×
107
            } else if (xmlData[i] === "%") {
225!
108
                throw new Error("Parameter entities are not supported");
×
109
            }
×
110
        }
225✔
111

225✔
112
        // Read entity value (internal entity)
225✔
113
        let entityValue = "";
225✔
114
        [i, entityValue] = this.readIdentifierVal(xmlData, i, "entity");
225✔
115

225✔
116
        // Validate entity size
225✔
117
        if (this.options.enabled !== false &&
230✔
118
            this.options.maxEntitySize &&
230✔
119
            entityValue.length > this.options.maxEntitySize) {
230✔
120
            throw new Error(
15✔
121
                `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
15✔
122
            );
15✔
123
        }
15✔
124

210✔
125
        i--;
210✔
126
        return [entityName, entityValue, i];
210✔
127
    }
230✔
128

5✔
129
    readNotationExp(xmlData, i) {
5✔
130
        // Skip leading whitespace after <!NOTATION
10✔
131
        i = skipWhitespace(xmlData, i);
10✔
132

10✔
133
        // Read notation name
10✔
134
        let notationName = "";
10✔
135
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
10✔
136
            notationName += xmlData[i];
45✔
137
            i++;
45✔
138
        }
45✔
139
        !this.suppressValidationErr && validateEntityName(notationName);
10✔
140

10✔
141
        // Skip whitespace after notation name
10✔
142
        i = skipWhitespace(xmlData, i);
10✔
143

10✔
144
        // Check identifier type (SYSTEM or PUBLIC)
10✔
145
        const identifierType = xmlData.substring(i, i + 6).toUpperCase();
10✔
146
        if (!this.suppressValidationErr && identifierType !== "SYSTEM" && identifierType !== "PUBLIC") {
10!
147
            throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
×
148
        }
×
149
        i += identifierType.length;
10✔
150

10✔
151
        // Skip whitespace after identifier type
10✔
152
        i = skipWhitespace(xmlData, i);
10✔
153

10✔
154
        // Read public identifier (if PUBLIC)
10✔
155
        let publicIdentifier = null;
10✔
156
        let systemIdentifier = null;
10✔
157

10✔
158
        if (identifierType === "PUBLIC") {
10✔
159
            [i, publicIdentifier] = this.readIdentifierVal(xmlData, i, "publicIdentifier");
10✔
160

10✔
161
            // Skip whitespace after public identifier
10✔
162
            i = skipWhitespace(xmlData, i);
10✔
163

10✔
164
            // Optionally read system identifier
10✔
165
            if (xmlData[i] === '"' || xmlData[i] === "'") {
10✔
166
                [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
5✔
167
            }
5✔
168
        } else if (identifierType === "SYSTEM") {
10!
169
            // Read system identifier (mandatory for SYSTEM)
×
170
            [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
×
171

×
172
            if (!this.suppressValidationErr && !systemIdentifier) {
×
173
                throw new Error("Missing mandatory system identifier for SYSTEM notation");
×
174
            }
×
175
        }
×
176

10✔
177
        return { notationName, publicIdentifier, systemIdentifier, index: --i };
10✔
178
    }
10✔
179

5✔
180
    readIdentifierVal(xmlData, i, type) {
5✔
181
        let identifierVal = "";
240✔
182
        const startChar = xmlData[i];
240✔
183
        if (startChar !== '"' && startChar !== "'") {
240!
184
            throw new Error(`Expected quoted string, found "${startChar}"`);
×
185
        }
×
186
        i++;
240✔
187

240✔
188
        while (i < xmlData.length && xmlData[i] !== startChar) {
240✔
189
            identifierVal += xmlData[i];
441,490✔
190
            i++;
441,490✔
191
        }
441,490✔
192

240✔
193
        if (xmlData[i] !== startChar) {
240!
194
            throw new Error(`Unterminated ${type} value`);
×
195
        }
×
196
        i++;
240✔
197
        return [i, identifierVal];
240✔
198
    }
240✔
199

5✔
200
    readElementExp(xmlData, i) {
5✔
201
        // <!ELEMENT br EMPTY>
20✔
202
        // <!ELEMENT div ANY>
20✔
203
        // <!ELEMENT title (#PCDATA)>
20✔
204
        // <!ELEMENT book (title, author+)>
20✔
205
        // <!ELEMENT name (content-model)>
20✔
206

20✔
207
        // Skip leading whitespace after <!ELEMENT
20✔
208
        i = skipWhitespace(xmlData, i);
20✔
209

20✔
210
        // Read element name
20✔
211
        let elementName = "";
20✔
212
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
20✔
213
            elementName += xmlData[i];
65✔
214
            i++;
65✔
215
        }
65✔
216

20✔
217
        // Validate element name
20✔
218
        if (!this.suppressValidationErr && !isName(elementName)) {
20!
219
            throw new Error(`Invalid element name: "${elementName}"`);
×
220
        }
×
221

20✔
222
        // Skip whitespace after element name
20✔
223
        i = skipWhitespace(xmlData, i);
20✔
224
        let contentModel = "";
20✔
225
        // Expect '(' to start content model
20✔
226
        if (xmlData[i] === "E" && hasSeq(xmlData, "MPTY", i)) i += 4;
20✔
227
        else if (xmlData[i] === "A" && hasSeq(xmlData, "NY", i)) i += 2;
15✔
228
        else if (xmlData[i] === "(") {
10✔
229
            i++; // Move past '('
10✔
230

10✔
231
            // Read content model
10✔
232
            while (i < xmlData.length && xmlData[i] !== ")") {
10✔
233
                contentModel += xmlData[i];
70✔
234
                i++;
70✔
235
            }
70✔
236
            if (xmlData[i] !== ")") {
10!
237
                throw new Error("Unterminated content model");
×
238
            }
×
239

10✔
240
        } else if (!this.suppressValidationErr) {
10!
241
            throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
×
242
        }
×
243

20✔
244
        return {
20✔
245
            elementName,
20✔
246
            contentModel: contentModel.trim(),
20✔
247
            index: i
20✔
248
        };
20✔
249
    }
20✔
250

5✔
251
    readAttlistExp(xmlData, i) {
5✔
252
        // Skip leading whitespace after <!ATTLIST
×
253
        i = skipWhitespace(xmlData, i);
×
254

×
255
        // Read element name
×
256
        let elementName = "";
×
257
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
×
258
            elementName += xmlData[i];
×
259
            i++;
×
260
        }
×
261

×
262
        // Validate element name
×
263
        validateEntityName(elementName)
×
264

×
265
        // Skip whitespace after element name
×
266
        i = skipWhitespace(xmlData, i);
×
267

×
268
        // Read attribute name
×
269
        let attributeName = "";
×
270
        while (i < xmlData.length && !/\s/.test(xmlData[i])) {
×
271
            attributeName += xmlData[i];
×
272
            i++;
×
273
        }
×
274

×
275
        // Validate attribute name
×
276
        if (!validateEntityName(attributeName)) {
×
277
            throw new Error(`Invalid attribute name: "${attributeName}"`);
×
278
        }
×
279

×
280
        // Skip whitespace after attribute name
×
281
        i = skipWhitespace(xmlData, i);
×
282

×
283
        // Read attribute type
×
284
        let attributeType = "";
×
285
        if (xmlData.substring(i, i + 8).toUpperCase() === "NOTATION") {
×
286
            attributeType = "NOTATION";
×
287
            i += 8; // Move past "NOTATION"
×
288

×
289
            // Skip whitespace after "NOTATION"
×
290
            i = skipWhitespace(xmlData, i);
×
291

×
292
            // Expect '(' to start the list of notations
×
293
            if (xmlData[i] !== "(") {
×
294
                throw new Error(`Expected '(', found "${xmlData[i]}"`);
×
295
            }
×
296
            i++; // Move past '('
×
297

×
298
            // Read the list of allowed notations
×
299
            let allowedNotations = [];
×
300
            while (i < xmlData.length && xmlData[i] !== ")") {
×
301
                let notation = "";
×
302
                while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
×
303
                    notation += xmlData[i];
×
304
                    i++;
×
305
                }
×
306

×
307
                // Validate notation name
×
308
                notation = notation.trim();
×
309
                if (!validateEntityName(notation)) {
×
310
                    throw new Error(`Invalid notation name: "${notation}"`);
×
311
                }
×
312

×
313
                allowedNotations.push(notation);
×
314

×
315
                // Skip '|' separator or exit loop
×
316
                if (xmlData[i] === "|") {
×
317
                    i++; // Move past '|'
×
318
                    i = skipWhitespace(xmlData, i); // Skip optional whitespace after '|'
×
319
                }
×
320
            }
×
321

×
322
            if (xmlData[i] !== ")") {
×
323
                throw new Error("Unterminated list of notations");
×
324
            }
×
325
            i++; // Move past ')'
×
326

×
327
            // Store the allowed notations as part of the attribute type
×
328
            attributeType += " (" + allowedNotations.join("|") + ")";
×
329
        } else {
×
330
            // Handle simple types (e.g., CDATA, ID, IDREF, etc.)
×
331
            while (i < xmlData.length && !/\s/.test(xmlData[i])) {
×
332
                attributeType += xmlData[i];
×
333
                i++;
×
334
            }
×
335

×
336
            // Validate simple attribute type
×
337
            const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
×
338
            if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
×
339
                throw new Error(`Invalid attribute type: "${attributeType}"`);
×
340
            }
×
341
        }
×
342

×
343
        // Skip whitespace after attribute type
×
344
        i = skipWhitespace(xmlData, i);
×
345

×
346
        // Read default value
×
347
        let defaultValue = "";
×
348
        if (xmlData.substring(i, i + 8).toUpperCase() === "#REQUIRED") {
×
349
            defaultValue = "#REQUIRED";
×
350
            i += 8;
×
351
        } else if (xmlData.substring(i, i + 7).toUpperCase() === "#IMPLIED") {
×
352
            defaultValue = "#IMPLIED";
×
353
            i += 7;
×
354
        } else {
×
355
            [i, defaultValue] = this.readIdentifierVal(xmlData, i, "ATTLIST");
×
356
        }
×
357

×
358
        return {
×
359
            elementName,
×
360
            attributeName,
×
361
            attributeType,
×
362
            defaultValue,
×
363
            index: i
×
364
        }
×
365
    }
×
366
}
5✔
367

5✔
368

5✔
369

5✔
370
const skipWhitespace = (data, index) => {
5✔
371
    while (index < data.length && /\s/.test(data[index])) {
535✔
372
        index++;
540✔
373
    }
540✔
374
    return index;
535✔
375
};
5✔
376

5✔
377

5✔
378

5✔
379
function hasSeq(data, seq, i) {
460✔
380
    for (let j = 0; j < seq.length; j++) {
460✔
381
        if (seq[j] !== data[i + j + 1]) return false;
2,345✔
382
    }
2,345✔
383
    return true;
300✔
384
}
460✔
385

5✔
386
function validateEntityName(name) {
240✔
387
    if (isName(name))
240✔
388
        return name;
240✔
389
    else
5✔
390
        throw new Error(`Invalid entity name ${name}`);
5✔
391
}
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