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

NaturalIntelligence / fast-xml-parser / 15517131831

08 Jun 2025 08:04AM UTC coverage: 97.547%. Remained the same
15517131831

push

github

amitguptagwl
deprecate in-built CLI

1105 of 1147 branches covered (96.34%)

8908 of 9132 relevant lines covered (97.55%)

390591.26 hits per line

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

99.06
/src/validator.js
1
'use strict';
5✔
2

5✔
3
import {getAllMatches, isName} from './util.js';
5✔
4

5✔
5
const defaultOptions = {
5✔
6
  allowBooleanAttributes: false, //A tag can have attributes without any value
5✔
7
  unpairedTags: []
5✔
8
};
5✔
9

5✔
10
//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
5✔
11
export function validate(xmlData, options) {
5✔
12
  options = Object.assign({}, defaultOptions, options);
675✔
13

675✔
14
  //xmlData = xmlData.replace(/(\r\n|\n|\r)/gm,"");//make it single line
675✔
15
  //xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag
675✔
16
  //xmlData = xmlData.replace(/(<!DOCTYPE[\s\w\"\.\/\-\:]+(\[.*\])*\s*>)/g,"");//Remove DOCTYPE
675✔
17
  const tags = [];
675✔
18
  let tagFound = false;
675✔
19

675✔
20
  //indicates that the root tag has been closed (aka. depth 0 has been reached)
675✔
21
  let reachedRoot = false;
675✔
22

675✔
23
  if (xmlData[0] === '\ufeff') {
675✔
24
    // check for byte order mark (BOM)
5✔
25
    xmlData = xmlData.substr(1);
5✔
26
  }
5✔
27
  
675✔
28
  for (let i = 0; i < xmlData.length; i++) {
675✔
29

26,961,345✔
30
    if (xmlData[i] === '<' && xmlData[i+1] === '?') {
26,961,345✔
31
      i+=2;
100✔
32
      i = readPI(xmlData,i);
100✔
33
      if (i.err) return i;
100✔
34
    }else if (xmlData[i] === '<') {
26,961,345✔
35
      //starting of tag
26,960,240✔
36
      //read until you reach to '>' avoiding any '>' in attribute value
26,960,240✔
37
      let tagStartPos = i;
26,960,240✔
38
      i++;
26,960,240✔
39
      
26,960,240✔
40
      if (xmlData[i] === '!') {
26,960,240✔
41
        i = readCommentAndCDATA(xmlData, i);
60✔
42
        continue;
60✔
43
      } else {
26,960,240✔
44
        let closingTag = false;
26,960,180✔
45
        if (xmlData[i] === '/') {
26,960,180✔
46
          //closing tag
13,134,280✔
47
          closingTag = true;
13,134,280✔
48
          i++;
13,134,280✔
49
        }
13,134,280✔
50
        //read tagname
26,960,180✔
51
        let tagName = '';
26,960,180✔
52
        for (; i < xmlData.length &&
26,960,180✔
53
          xmlData[i] !== '>' &&
26,960,180✔
54
          xmlData[i] !== ' ' &&
26,960,180✔
55
          xmlData[i] !== '\t' &&
26,960,180✔
56
          xmlData[i] !== '\n' &&
26,960,180✔
57
          xmlData[i] !== '\r'; i++
26,960,180✔
58
        ) {
26,960,180✔
59
          tagName += xmlData[i];
178,348,615✔
60
        }
178,348,615✔
61
        tagName = tagName.trim();
26,960,180✔
62
        //console.log(tagName);
26,960,180✔
63

26,960,180✔
64
        if (tagName[tagName.length - 1] === '/') {
26,960,180✔
65
          //self closing tag without attributes
70✔
66
          tagName = tagName.substring(0, tagName.length - 1);
70✔
67
          //continue;
70✔
68
          i--;
70✔
69
        }
70✔
70
        if (!validateTagName(tagName)) {
26,960,180✔
71
          let msg;
50✔
72
          if (tagName.trim().length === 0) {
50✔
73
            msg = "Invalid space after '<'.";
15✔
74
          } else {
50✔
75
            msg = "Tag '"+tagName+"' is an invalid name.";
35✔
76
          }
35✔
77
          return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
50✔
78
        }
50✔
79

26,960,130✔
80
        const result = readAttributeStr(xmlData, i);
26,960,130✔
81
        if (result === false) {
26,960,180✔
82
          return getErrorObject('InvalidAttr', "Attributes for '"+tagName+"' have open quote.", getLineNumberForPosition(xmlData, i));
5✔
83
        }
5✔
84
        let attrStr = result.value;
26,960,125✔
85
        i = result.index;
26,960,125✔
86

26,960,125✔
87
        if (attrStr[attrStr.length - 1] === '/') {
26,960,180✔
88
          //self closing tag
691,375✔
89
          const attrStrStart = i - attrStr.length;
691,375✔
90
          attrStr = attrStr.substring(0, attrStr.length - 1);
691,375✔
91
          const isValid = validateAttributeString(attrStr, options);
691,375✔
92
          if (isValid === true) {
691,375✔
93
            tagFound = true;
691,345✔
94
            //continue; //text may presents after self closing tag
691,345✔
95
          } else {
691,375✔
96
            //the result from the nested function returns the position of the error within the attribute
30✔
97
            //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
30✔
98
            //this gives us the absolute index in the entire xml, which we can use to find the line at last
30✔
99
            return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
30✔
100
          }
30✔
101
        } else if (closingTag) {
26,960,180✔
102
          if (!result.tagClosed) {
13,134,270✔
103
            return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
10✔
104
          } else if (attrStr.trim().length > 0) {
13,134,270✔
105
            return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
10✔
106
          } else if (tags.length === 0) {
13,134,260✔
107
            return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
5✔
108
          } else {
13,134,250✔
109
            const otg = tags.pop();
13,134,245✔
110
            if (tagName !== otg.tagName) {
13,134,245✔
111
              let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
45✔
112
              return getErrorObject('InvalidTag',
45✔
113
                "Expected closing tag '"+otg.tagName+"' (opened in line "+openPos.line+", col "+openPos.col+") instead of closing tag '"+tagName+"'.",
45✔
114
                getLineNumberForPosition(xmlData, tagStartPos));
45✔
115
            }
45✔
116

13,134,200✔
117
            //when there are no more tags, we reached the root level.
13,134,200✔
118
            if (tags.length == 0) {
13,134,245✔
119
              reachedRoot = true;
365✔
120
            }
365✔
121
          }
13,134,245✔
122
        } else {
26,268,750✔
123
          const isValid = validateAttributeString(attrStr, options);
13,134,480✔
124
          if (isValid !== true) {
13,134,480✔
125
            //the result from the nested function returns the position of the error within the attribute
50✔
126
            //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
50✔
127
            //this gives us the absolute index in the entire xml, which we can use to find the line at last
50✔
128
            return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
50✔
129
          }
50✔
130

13,134,430✔
131
          //if the root level has been reached before ...
13,134,430✔
132
          if (reachedRoot === true) {
13,134,480✔
133
            return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
15✔
134
          } else if(options.unpairedTags.indexOf(tagName) !== -1){
13,134,480✔
135
            //don't push into stack
5✔
136
          } else {
13,134,415✔
137
            tags.push({tagName, tagStartPos});
13,134,410✔
138
          }
13,134,410✔
139
          tagFound = true;
13,134,415✔
140
        }
13,134,415✔
141

26,959,960✔
142
        //skip tag text value
26,959,960✔
143
        //It may include comments and CDATA value
26,959,960✔
144
        for (i++; i < xmlData.length; i++) {
26,960,180✔
145
          if (xmlData[i] === '<') {
251,271,655✔
146
            if (xmlData[i + 1] === '!') {
26,959,715✔
147
              //comment or CADATA
170✔
148
              i++;
170✔
149
              i = readCommentAndCDATA(xmlData, i);
170✔
150
              continue;
170✔
151
            } else if (xmlData[i+1] === '?') {
26,959,715✔
152
              i = readPI(xmlData, ++i);
15✔
153
              if (i.err) return i;
15!
154
            } else{
26,959,545✔
155
              break;
26,959,530✔
156
            }
26,959,530✔
157
          } else if (xmlData[i] === '&') {
251,271,655✔
158
            const afterAmp = validateAmpersand(xmlData, i);
355✔
159
            if (afterAmp == -1)
355✔
160
              return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
355✔
161
            i = afterAmp;
320✔
162
          }else{
224,311,940✔
163
            if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
224,311,585✔
164
              return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
5✔
165
            }
5✔
166
          }
224,311,585✔
167
        } //end of reading tag text value
26,960,180✔
168
        if (xmlData[i] === '<') {
26,960,180✔
169
          i--;
26,959,530✔
170
        }
26,959,530✔
171
      }
26,960,180✔
172
    } else {
26,961,245✔
173
      if ( isWhiteSpace(xmlData[i])) {
1,005✔
174
        continue;
995✔
175
      }
995✔
176
      return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i));
10✔
177
    }
10✔
178
  }
26,961,345✔
179

400✔
180
  if (!tagFound) {
675✔
181
    return getErrorObject('InvalidXml', 'Start tag expected.', 1);
10✔
182
  }else if (tags.length == 1) {
675✔
183
      return getErrorObject('InvalidTag', "Unclosed tag '"+tags[0].tagName+"'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
25✔
184
  }else if (tags.length > 0) {
390!
185
      return getErrorObject('InvalidXml', "Invalid '"+
×
186
          JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '')+
×
187
          "' found.", {line: 1, col: 1});
×
188
  }
×
189

365✔
190
  return true;
365✔
191
};
5✔
192

5✔
193
function isWhiteSpace(char){
1,110✔
194
  return char === ' ' || char === '\t' || char === '\n'  || char === '\r';
1,110✔
195
}
1,110✔
196
/**
5✔
197
 * Read Processing insstructions and skip
5✔
198
 * @param {*} xmlData
5✔
199
 * @param {*} i
5✔
200
 */
5✔
201
function readPI(xmlData, i) {
115✔
202
  const start = i;
115✔
203
  for (; i < xmlData.length; i++) {
115✔
204
    if (xmlData[i] == '?' || xmlData[i] == ' ') {
2,870✔
205
      //tagname
310✔
206
      const tagname = xmlData.substr(start, i - start);
310✔
207
      if (i > 5 && tagname === 'xml') {
310✔
208
        return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));
5✔
209
      } else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {
310✔
210
        //check if valid attribut string
110✔
211
        i++;
110✔
212
        break;
110✔
213
      } else {
305✔
214
        continue;
195✔
215
      }
195✔
216
    }
310✔
217
  }
2,870✔
218
  return i;
110✔
219
}
115✔
220

5✔
221
function readCommentAndCDATA(xmlData, i) {
230✔
222
  if (xmlData.length > i + 5 && xmlData[i + 1] === '-' && xmlData[i + 2] === '-') {
230✔
223
    //comment
65✔
224
    for (i += 3; i < xmlData.length; i++) {
65✔
225
      if (xmlData[i] === '-' && xmlData[i + 1] === '-' && xmlData[i + 2] === '>') {
2,315✔
226
        i += 2;
60✔
227
        break;
60✔
228
      }
60✔
229
    }
2,315✔
230
  } else if (
230✔
231
    xmlData.length > i + 8 &&
165✔
232
    xmlData[i + 1] === 'D' &&
165✔
233
    xmlData[i + 2] === 'O' &&
165✔
234
    xmlData[i + 3] === 'C' &&
165✔
235
    xmlData[i + 4] === 'T' &&
165✔
236
    xmlData[i + 5] === 'Y' &&
165✔
237
    xmlData[i + 6] === 'P' &&
165✔
238
    xmlData[i + 7] === 'E'
30✔
239
  ) {
165✔
240
    let angleBracketsCount = 1;
30✔
241
    for (i += 8; i < xmlData.length; i++) {
30✔
242
      if (xmlData[i] === '<') {
2,480✔
243
        angleBracketsCount++;
30✔
244
      } else if (xmlData[i] === '>') {
2,480✔
245
        angleBracketsCount--;
60✔
246
        if (angleBracketsCount === 0) {
60✔
247
          break;
30✔
248
        }
30✔
249
      }
60✔
250
    }
2,480✔
251
  } else if (
165✔
252
    xmlData.length > i + 9 &&
135✔
253
    xmlData[i + 1] === '[' &&
135✔
254
    xmlData[i + 2] === 'C' &&
135✔
255
    xmlData[i + 3] === 'D' &&
135✔
256
    xmlData[i + 4] === 'A' &&
135✔
257
    xmlData[i + 5] === 'T' &&
135✔
258
    xmlData[i + 6] === 'A' &&
135✔
259
    xmlData[i + 7] === '['
130✔
260
  ) {
135✔
261
    for (i += 8; i < xmlData.length; i++) {
130✔
262
      if (xmlData[i] === ']' && xmlData[i + 1] === ']' && xmlData[i + 2] === '>') {
1,650✔
263
        i += 2;
130✔
264
        break;
130✔
265
      }
130✔
266
    }
1,650✔
267
  }
130✔
268

230✔
269
  return i;
230✔
270
}
230✔
271

5✔
272
const doubleQuote = '"';
5✔
273
const singleQuote = "'";
5✔
274

5✔
275
/**
5✔
276
 * Keep reading xmlData until '<' is found outside the attribute value.
5✔
277
 * @param {string} xmlData
5✔
278
 * @param {number} i
5✔
279
 */
5✔
280
function readAttributeStr(xmlData, i) {
26,960,130✔
281
  let attrStr = '';
26,960,130✔
282
  let startChar = '';
26,960,130✔
283
  let tagClosed = false;
26,960,130✔
284
  for (; i < xmlData.length; i++) {
26,960,130✔
285
    if (xmlData[i] === doubleQuote || xmlData[i] === singleQuote) {
66,729,080✔
286
      if (startChar === '') {
4,840,015✔
287
        startChar = xmlData[i];
2,420,005✔
288
      } else if (startChar !== xmlData[i]) {
4,840,015✔
289
        //if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa
10✔
290
      } else {
2,420,010✔
291
        startChar = '';
2,420,000✔
292
      }
2,420,000✔
293
    } else if (xmlData[i] === '>') {
66,729,080✔
294
      if (startChar === '') {
26,960,160✔
295
        tagClosed = true;
26,960,110✔
296
        break;
26,960,110✔
297
      }
26,960,110✔
298
    }
26,960,160✔
299
    attrStr += xmlData[i];
39,768,970✔
300
  }
39,768,970✔
301
  if (startChar !== '') {
26,960,130✔
302
    return false;
5✔
303
  }
5✔
304

26,960,125✔
305
  return {
26,960,125✔
306
    value: attrStr,
26,960,125✔
307
    index: i,
26,960,125✔
308
    tagClosed: tagClosed
26,960,125✔
309
  };
26,960,125✔
310
}
26,960,130✔
311

5✔
312
/**
5✔
313
 * Select all the attributes whether valid or invalid.
5✔
314
 */
5✔
315
const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g');
5✔
316

5✔
317
//attr, ="sd", a="amit's", a="sd"b="saf", ab  cd=""
5✔
318

5✔
319
function validateAttributeString(attrStr, options) {
13,825,855✔
320
  //console.log("start:"+attrStr+":end");
13,825,855✔
321

13,825,855✔
322
  //if(attrStr.trim().length === 0) return true; //empty string
13,825,855✔
323

13,825,855✔
324
  const matches = getAllMatches(attrStr, validAttrStrRegxp);
13,825,855✔
325
  const attrNames = {};
13,825,855✔
326

13,825,855✔
327
  for (let i = 0; i < matches.length; i++) {
13,825,855✔
328
    if (matches[i][1].length === 0) {
2,420,010✔
329
      //nospace before attribute name: a="sd"b="saf"
15✔
330
      return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i]))
15✔
331
    } else if (matches[i][3] !== undefined && matches[i][4] === undefined) {
2,420,010✔
332
      return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' is without value.", getPositionFromMatch(matches[i]));
5✔
333
    } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
2,419,995✔
334
      //independent attribute: ab
40✔
335
      return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i]));
40✔
336
    }
40✔
337
    /* else if(matches[i][6] === undefined){//attribute without value: ab=
2,419,950✔
338
                    return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
2,419,950✔
339
                } */
2,419,950✔
340
    const attrName = matches[i][2];
2,419,950✔
341
    if (!validateAttrName(attrName)) {
2,420,010✔
342
      return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(matches[i]));
10✔
343
    }
10✔
344
    if (!attrNames.hasOwnProperty(attrName)) {
2,420,010✔
345
      //check for duplicate attribute.
2,419,930✔
346
      attrNames[attrName] = 1;
2,419,930✔
347
    } else {
2,420,010✔
348
      return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(matches[i]));
10✔
349
    }
10✔
350
  }
2,420,010✔
351

13,825,775✔
352
  return true;
13,825,775✔
353
}
13,825,855✔
354

5✔
355
function validateNumberAmpersand(xmlData, i) {
310✔
356
  let re = /\d/;
310✔
357
  if (xmlData[i] === 'x') {
310✔
358
    i++;
290✔
359
    re = /[\da-fA-F]/;
290✔
360
  }
290✔
361
  for (; i < xmlData.length; i++) {
310✔
362
    if (xmlData[i] === ';')
1,250✔
363
      return i;
1,250✔
364
    if (!xmlData[i].match(re))
960✔
365
      break;
1,250✔
366
  }
1,250✔
367
  return -1;
20✔
368
}
310✔
369

5✔
370
function validateAmpersand(xmlData, i) {
355✔
371
  // https://www.w3.org/TR/xml/#dt-charref
355✔
372
  i++;
355✔
373
  if (xmlData[i] === ';')
355✔
374
    return -1;
355!
375
  if (xmlData[i] === '#') {
355✔
376
    i++;
310✔
377
    return validateNumberAmpersand(xmlData, i);
310✔
378
  }
310✔
379
  let count = 0;
45✔
380
  for (; i < xmlData.length; i++, count++) {
355✔
381
    if (xmlData[i].match(/\w/) && count < 20)
320✔
382
      continue;
320✔
383
    if (xmlData[i] === ';')
45✔
384
      break;
320✔
385
    return -1;
15✔
386
  }
15✔
387
  return i;
30✔
388
}
355✔
389

5✔
390
function getErrorObject(code, message, lineNumber) {
390✔
391
  return {
390✔
392
    err: {
390✔
393
      code: code,
390✔
394
      msg: message,
390✔
395
      line: lineNumber.line || lineNumber,
390✔
396
      col: lineNumber.col,
390✔
397
    },
390✔
398
  };
390✔
399
}
390✔
400

5✔
401
function validateAttrName(attrName) {
2,419,950✔
402
  return isName(attrName);
2,419,950✔
403
}
2,419,950✔
404

5✔
405
// const startsWithXML = /^xml/i;
5✔
406

5✔
407
function validateTagName(tagname) {
26,960,180✔
408
  return isName(tagname) /* && !tagname.match(startsWithXML) */;
26,960,180✔
409
}
26,960,180✔
410

5✔
411
//this function returns the line number for the character at the given index
5✔
412
function getLineNumberForPosition(xmlData, index) {
345✔
413
  const lines = xmlData.substring(0, index).split(/\r?\n/);
345✔
414
  return {
345✔
415
    line: lines.length,
345✔
416

345✔
417
    // column number is last line's length + 1, because column numbering starts at 1:
345✔
418
    col: lines[lines.length - 1].length + 1
345✔
419
  };
345✔
420
}
345✔
421

5✔
422
//this function returns the position of the first character of match within attrStr
5✔
423
function getPositionFromMatch(match) {
80✔
424
  return match.startIndex + match[1].length;
80✔
425
}
80✔
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