Find the PDF filename in a URL hash in two linear steps

Searching for a name followed by ".pdf" is quadratic on a hash which
doesn't contain one, so locate the last ".pdf" first and then extend
it to the left.
This commit is contained in:
calixteman 2026-08-01 16:03:43 +02:00
parent 7fc7072f9c
commit 7862875438
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
2 changed files with 40 additions and 4 deletions

View File

@ -188,10 +188,22 @@ function getPdfFilenameFromUrl(url, defaultFilename = "document.pdf") {
}
if (newURL.hash) {
const reFilename = /[^/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
const hashFilename = reFilename.exec(newURL.hash);
if (hashFilename) {
return decode(hashFilename[0]);
// Locate the last ".pdf" and then extend it to the left, up to the closest
// separator. Both steps are linear, whereas a single pattern starting with
// `[^/?#=]+` is quadratic on a hash which contains no ".pdf" at all.
const { hash } = newURL;
let extensionStart = -1;
for (const { index } of hash.matchAll(/\.pdf\b/gi)) {
extensionStart = index;
}
if (extensionStart > 0) {
let filenameStart = extensionStart;
while (filenameStart > 0 && !"/?#=".includes(hash[filenameStart - 1])) {
filenameStart--;
}
if (filenameStart < extensionStart) {
return decode(hash.slice(filenameStart, extensionStart + 4));
}
}
}

View File

@ -122,6 +122,30 @@ describe("display_utils", function () {
expect(
getPdfFilenameFromUrl("http://www.example.com/pdfs/pdf.html#file2.pdf")
).toEqual("file2.pdf");
// Only the last ".pdf" of the hash is used.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#a.pdf/b.pdf")).toEqual(
"b.pdf"
);
// A ".pdf" which isn't preceded by a name is ignored.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#=.pdf")).toEqual(
"document.pdf"
);
// An invalid last ".pdf" prevents an earlier valid one from being used.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#a.pdf/=.pdf")).toEqual(
"document.pdf"
);
});
it("gets PDF filename from a long hash string efficiently", function () {
// Scanning the hash for a name is quadratic when it contains no ".pdf".
const url = `/pdfs/pdfs.html#${"a".repeat(200000)}`;
const startTime = performance.now();
const filename = getPdfFilenameFromUrl(url);
const duration = performance.now() - startTime;
expect(filename).toEqual("document.pdf");
expect(duration).toBeLessThan(1000);
});
it("gets correct PDF filename when multiple ones are present", function () {