Anchor the regex used to extract the XFA path positions

Matching the name with a leading `.+` is quadratic in the length of
a component which doesn't end with a position, and every AcroForm
field name goes through this.
This commit is contained in:
calixteman 2026-08-01 16:00:34 +02:00
parent d0779c411e
commit 92d027eaeb
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
2 changed files with 28 additions and 1 deletions

View File

@ -341,7 +341,8 @@ function lookupNormalRect(arr, fallback) {
* each part of the path.
*/
function parseXFAPath(path) {
const positionPattern = /(.+)\[(\d+)\]$/;
// Anchoring prevents retrying the match at every character.
const positionPattern = /^(.+)\[(\d+)\]$/;
return path.split(".").map(component => {
const m = component.match(positionPattern);
if (m) {

View File

@ -244,6 +244,32 @@ describe("core_utils", function () {
{ name: "BAR", pos: 456 },
]);
});
it("should ignore a malformed position", function () {
expect(parseXFAPath("foo[].bar[1x].oof[].[3]")).toEqual([
{ name: "foo[]", pos: 0 },
{ name: "bar[1x]", pos: 0 },
{ name: "oof[]", pos: 0 },
{ name: "[3]", pos: 0 },
]);
});
it("should keep the longest name when a component has several brackets", function () {
expect(parseXFAPath("foo[1][2]")).toEqual([{ name: "foo[1]", pos: 2 }]);
});
it("should handle a long component efficiently", function () {
// Looking for the position with a leading `.+` is quadratic in the
// length of a component which doesn't end with one.
const name = "a".repeat(200000);
const startTime = performance.now();
const parsedPath = parseXFAPath(name);
const duration = performance.now() - startTime;
expect(parsedPath).toEqual([{ name, pos: 0 }]);
expect(duration).toBeLessThan(1000);
});
});
describe("recoverJsURL", function () {