Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/fxp.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/fxp.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/fxp.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/fxparser.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/fxparser.min.js.map

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions spec/large_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,31 @@ describe("XMLParser", function() {
const result = XMLValidator.validate(svgData);
expect(result).toBe(true);
});

// Regression for https://github.com/NaturalIntelligence/fast-xml-parser/issues/817
// 5.5.11's tagExpWithClosingIndex accumulated char codes into an array and
// returned via String.fromCharCode(...chars). For a tag expression large
// enough, the spread exceeds V8's argument-count limit and surfaces as
// "RangeError: Maximum call stack size exceeded".
it("should parse a tag with a very long attribute value without stack overflow", function() {
const longValue = "x".repeat(200000);
const xmlData = `<root><item attr="${longValue}"/></root>`;

const parser = new XMLParser({ ignoreAttributes: false });
let result;
expect(() => { result = parser.parse(xmlData); }).not.toThrow();
expect(result.root.item["@_attr"]).toBe(longValue);
});

// Tabs inside quoted attribute values must be preserved verbatim.
// Only tabs outside quoted attributes (between attributes) are
// normalised to spaces. The spacing between attributes is
// implementation detail and is not asserted here.
it("should preserve tab characters inside quoted attribute values", function() {
const xmlData = '<root><item a="x\ty"/></root>';

const parser = new XMLParser({ ignoreAttributes: false });
const result = parser.parse(xmlData);
expect(result.root.item["@_a"]).toBe("x\ty");
});
});
47 changes: 40 additions & 7 deletions src/xmlparser/OrderedObjParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -654,33 +654,66 @@ function isItStopNode() {
*/
function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
let attrBoundary = 0;
const chars = [];
let hasTabInQuote = false;
const len = xmlData.length;
const closeCode0 = closingChar.charCodeAt(0);
const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
let closeIndex = -1;

// First pass: locate the closing delimiter, tracking quote boundaries so
// they don't mask the close. Note any \t seen inside a quoted attribute
// value — those must be preserved, so we can't bulk-replace tabs.
for (let index = i; index < len; index++) {
const code = xmlData.charCodeAt(index);

if (attrBoundary) {
if (code === attrBoundary) attrBoundary = 0;
else if (code === 9) hasTabInQuote = true;
} else if (code === 34 || code === 39) { // " or '
attrBoundary = code;
} else if (code === closeCode0) {
if (closeCode1 !== -1) {
if (xmlData.charCodeAt(index + 1) === closeCode1) {
return { data: String.fromCharCode(...chars), index };
closeIndex = index;
break;
}
} else {
return { data: String.fromCharCode(...chars), index };
closeIndex = index;
break;
}
} else if (code === 9) { // \t
chars.push(32); // space
continue;
}
}

if (closeIndex === -1) return;

chars.push(code);
const raw = xmlData.substring(i, closeIndex);

// Fast path: any tabs are guaranteed to be outside quoted attr values,
// so bulk-replace them with spaces.
if (!hasTabInQuote) {
return { data: raw.replace(/\t/g, " "), index: closeIndex };
}

// Rare path: the tag expression has at least one tab inside a quoted
// attribute value. Walk it and replace only the tabs that fall outside
// quote boundaries.
let tagExp = "";
let boundary = 0;
for (let k = 0; k < raw.length; k++) {
const code = raw.charCodeAt(k);
if (boundary) {
if (code === boundary) boundary = 0;
tagExp += raw[k];
} else if (code === 34 || code === 39) {
boundary = code;
tagExp += raw[k];
} else if (code === 9) {
tagExp += " ";
} else {
tagExp += raw[k];
}
}
return { data: tagExp, index: closeIndex };
}

function findClosingIndex(xmlData, str, i, errMsg) {
Expand Down