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

NaturalIntelligence / fast-xml-parser / 13532857399

25 Feb 2025 11:15PM UTC coverage: 98.946% (+0.7%) from 98.217%
13532857399

push

github

web-flow
Update node.js.yml

1087 of 1120 branches covered (97.05%)

8639 of 8731 relevant lines covered (98.95%)

408524.79 hits per line

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

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

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

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

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

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

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

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

21,568,972✔
30
    if (xmlData[i] === '<' && xmlData[i+1] === '?') {
21,568,972✔
31
      i+=2;
80✔
32
      i = readPI(xmlData,i);
80✔
33
      if (i.err) return i;
80✔
34
    }else if (xmlData[i] === '<') {
21,568,972✔
35
      //starting of tag
21,568,192✔
36
      //read until you reach to '>' avoiding any '>' in attribute value
21,568,192✔
37
      let tagStartPos = i;
21,568,192✔
38
      i++;
21,568,192✔
39
      
21,568,192✔
40
      if (xmlData[i] === '!') {
21,568,192✔
41
        i = readCommentAndCDATA(xmlData, i);
48✔
42
        continue;
48✔
43
      } else {
21,568,192✔
44
        let closingTag = false;
21,568,144✔
45
        if (xmlData[i] === '/') {
21,568,144✔
46
          //closing tag
10,507,424✔
47
          closingTag = true;
10,507,424✔
48
          i++;
10,507,424✔
49
        }
10,507,424✔
50
        //read tagname
21,568,144✔
51
        let tagName = '';
21,568,144✔
52
        for (; i < xmlData.length &&
21,568,144✔
53
          xmlData[i] !== '>' &&
21,568,144✔
54
          xmlData[i] !== ' ' &&
21,568,144✔
55
          xmlData[i] !== '\t' &&
21,568,144✔
56
          xmlData[i] !== '\n' &&
21,568,144✔
57
          xmlData[i] !== '\r'; i++
21,568,144✔
58
        ) {
21,568,144✔
59
          tagName += xmlData[i];
142,678,892✔
60
        }
142,678,892✔
61
        tagName = tagName.trim();
21,568,144✔
62
        //console.log(tagName);
21,568,144✔
63

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

21,568,104✔
80
        const result = readAttributeStr(xmlData, i);
21,568,104✔
81
        if (result === false) {
21,568,144✔
82
          return getErrorObject('InvalidAttr', "Attributes for '"+tagName+"' have open quote.", getLineNumberForPosition(xmlData, i));
4✔
83
        }
4✔
84
        let attrStr = result.value;
21,568,100✔
85
        i = result.index;
21,568,100✔
86

21,568,100✔
87
        if (attrStr[attrStr.length - 1] === '/') {
21,568,144✔
88
          //self closing tag
553,100✔
89
          const attrStrStart = i - attrStr.length;
553,100✔
90
          attrStr = attrStr.substring(0, attrStr.length - 1);
553,100✔
91
          const isValid = validateAttributeString(attrStr, options);
553,100✔
92
          if (isValid === true) {
553,100✔
93
            tagFound = true;
553,076✔
94
            //continue; //text may presents after self closing tag
553,076✔
95
          } else {
553,100✔
96
            //the result from the nested function returns the position of the error within the attribute
24✔
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
24✔
98
            //this gives us the absolute index in the entire xml, which we can use to find the line at last
24✔
99
            return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
24✔
100
          }
24✔
101
        } else if (closingTag) {
21,568,144✔
102
          if (!result.tagClosed) {
10,507,416✔
103
            return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
8✔
104
          } else if (attrStr.trim().length > 0) {
10,507,416✔
105
            return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
8✔
106
          } else if (tags.length === 0) {
10,507,408✔
107
            return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
4✔
108
          } else {
10,507,400✔
109
            const otg = tags.pop();
10,507,396✔
110
            if (tagName !== otg.tagName) {
10,507,396✔
111
              let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
36✔
112
              return getErrorObject('InvalidTag',
36✔
113
                "Expected closing tag '"+otg.tagName+"' (opened in line "+openPos.line+", col "+openPos.col+") instead of closing tag '"+tagName+"'.",
36✔
114
                getLineNumberForPosition(xmlData, tagStartPos));
36✔
115
            }
36✔
116

10,507,360✔
117
            //when there are no more tags, we reached the root level.
10,507,360✔
118
            if (tags.length == 0) {
10,507,396✔
119
              reachedRoot = true;
292✔
120
            }
292✔
121
          }
10,507,396✔
122
        } else {
21,015,000✔
123
          const isValid = validateAttributeString(attrStr, options);
10,507,584✔
124
          if (isValid !== true) {
10,507,584✔
125
            //the result from the nested function returns the position of the error within the attribute
40✔
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
40✔
127
            //this gives us the absolute index in the entire xml, which we can use to find the line at last
40✔
128
            return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
40✔
129
          }
40✔
130

10,507,544✔
131
          //if the root level has been reached before ...
10,507,544✔
132
          if (reachedRoot === true) {
10,507,584✔
133
            return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
12✔
134
          } else if(options.unpairedTags.indexOf(tagName) !== -1){
10,507,584✔
135
            //don't push into stack
4✔
136
          } else {
10,507,532✔
137
            tags.push({tagName, tagStartPos});
10,507,528✔
138
          }
10,507,528✔
139
          tagFound = true;
10,507,532✔
140
        }
10,507,532✔
141

21,567,968✔
142
        //skip tag text value
21,567,968✔
143
        //It may include comments and CDATA value
21,567,968✔
144
        for (i++; i < xmlData.length; i++) {
21,568,144✔
145
          if (xmlData[i] === '<') {
201,017,324✔
146
            if (xmlData[i + 1] === '!') {
21,567,772✔
147
              //comment or CADATA
136✔
148
              i++;
136✔
149
              i = readCommentAndCDATA(xmlData, i);
136✔
150
              continue;
136✔
151
            } else if (xmlData[i+1] === '?') {
21,567,772✔
152
              i = readPI(xmlData, ++i);
12✔
153
              if (i.err) return i;
12!
154
            } else{
21,567,636✔
155
              break;
21,567,624✔
156
            }
21,567,624✔
157
          } else if (xmlData[i] === '&') {
201,017,324✔
158
            const afterAmp = validateAmpersand(xmlData, i);
284✔
159
            if (afterAmp == -1)
284✔
160
              return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
284✔
161
            i = afterAmp;
256✔
162
          }else{
179,449,552✔
163
            if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
179,449,268✔
164
              return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
4✔
165
            }
4✔
166
          }
179,449,268✔
167
        } //end of reading tag text value
21,568,144✔
168
        if (xmlData[i] === '<') {
21,568,144✔
169
          i--;
21,567,624✔
170
        }
21,567,624✔
171
      }
21,568,144✔
172
    } else {
21,568,892✔
173
      if ( isWhiteSpace(xmlData[i])) {
700✔
174
        continue;
692✔
175
      }
692✔
176
      return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i));
8✔
177
    }
8✔
178
  }
21,568,972✔
179

320✔
180
  if (!tagFound) {
540✔
181
    return getErrorObject('InvalidXml', 'Start tag expected.', 1);
8✔
182
  }else if (tags.length == 1) {
540✔
183
      return getErrorObject('InvalidTag', "Unclosed tag '"+tags[0].tagName+"'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
20✔
184
  }else if (tags.length > 0) {
312!
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

292✔
190
  return true;
292✔
191
};
4✔
192

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

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

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

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

4✔
275
/**
4✔
276
 * Keep reading xmlData until '<' is found outside the attribute value.
4✔
277
 * @param {string} xmlData
4✔
278
 * @param {number} i
4✔
279
 */
4✔
280
function readAttributeStr(xmlData, i) {
21,568,104✔
281
  let attrStr = '';
21,568,104✔
282
  let startChar = '';
21,568,104✔
283
  let tagClosed = false;
21,568,104✔
284
  for (; i < xmlData.length; i++) {
21,568,104✔
285
    if (xmlData[i] === doubleQuote || xmlData[i] === singleQuote) {
53,383,264✔
286
      if (startChar === '') {
3,872,012✔
287
        startChar = xmlData[i];
1,936,004✔
288
      } else if (startChar !== xmlData[i]) {
3,872,012✔
289
        //if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa
8✔
290
      } else {
1,936,008✔
291
        startChar = '';
1,936,000✔
292
      }
1,936,000✔
293
    } else if (xmlData[i] === '>') {
53,383,264✔
294
      if (startChar === '') {
21,568,128✔
295
        tagClosed = true;
21,568,088✔
296
        break;
21,568,088✔
297
      }
21,568,088✔
298
    }
21,568,128✔
299
    attrStr += xmlData[i];
31,815,176✔
300
  }
31,815,176✔
301
  if (startChar !== '') {
21,568,104✔
302
    return false;
4✔
303
  }
4✔
304

21,568,100✔
305
  return {
21,568,100✔
306
    value: attrStr,
21,568,100✔
307
    index: i,
21,568,100✔
308
    tagClosed: tagClosed
21,568,100✔
309
  };
21,568,100✔
310
}
21,568,104✔
311

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

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

4✔
319
function validateAttributeString(attrStr, options) {
11,060,684✔
320
  //console.log("start:"+attrStr+":end");
11,060,684✔
321

11,060,684✔
322
  //if(attrStr.trim().length === 0) return true; //empty string
11,060,684✔
323

11,060,684✔
324
  const matches = getAllMatches(attrStr, validAttrStrRegxp);
11,060,684✔
325
  const attrNames = {};
11,060,684✔
326

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

11,060,620✔
352
  return true;
11,060,620✔
353
}
11,060,684✔
354

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

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

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

4✔
401
function validateAttrName(attrName) {
1,935,960✔
402
  return isName(attrName);
1,935,960✔
403
}
1,935,960✔
404

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

4✔
407
function validateTagName(tagname) {
21,568,144✔
408
  return isName(tagname) /* && !tagname.match(startsWithXML) */;
21,568,144✔
409
}
21,568,144✔
410

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

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

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