diff --git a/.github/workflows/publish_website.yml b/.github/workflows/publish_website.yml index e3141f9505bcb..f9c5cf28de35b 100644 --- a/.github/workflows/publish_website.yml +++ b/.github/workflows/publish_website.yml @@ -6,6 +6,12 @@ on: permissions: contents: read +# Allow only one concurrent deployment, without cancelling in-progress runs, so +# that an in-flight Pages deployment is allowed to finish before the next starts. +concurrency: + group: "pages" + cancel-in-progress: false + jobs: build: name: Build diff --git a/src/core/core_utils.js b/src/core/core_utils.js index 5556c9e1510eb..5360d9ae274b7 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -376,7 +376,7 @@ function escapePDFName(str) { if (start < i) { buffer.push(str.substring(start, i)); } - buffer.push(`#${char.toString(16)}`); + buffer.push(`#${char.toString(16).padStart(2, "0")}`); start = i + 1; } } diff --git a/test/unit/core_utils_spec.js b/test/unit/core_utils_spec.js index a9c6bd8f040f8..6abfae364fb8d 100644 --- a/test/unit/core_utils_spec.js +++ b/test/unit/core_utils_spec.js @@ -290,6 +290,11 @@ describe("core_utils", function () { "#23#28#29#3c#3e#5b#5d#7b#7d#2f#25" ); }); + + it("should escape control characters using two hexadecimal digits", function () { + expect(escapePDFName("\x00\x09\x0a\x1f")).toEqual("#00#09#0a#1f"); + expect(escapePDFName("a\tb")).toEqual("a#09b"); + }); }); describe("escapeString", function () { diff --git a/web/app.js b/web/app.js index a4c7846368deb..a4bb42c9e2d24 100644 --- a/web/app.js +++ b/web/app.js @@ -1207,7 +1207,7 @@ const PDFViewerApplication = { this.pdfViewer.setDocument(null); this.pdfLinkService.setDocument(null); this.pdfDocumentProperties?.setDocument(null); - this.signaturePropertiesManager?.reset(); + this.signaturePropertiesManager?.setDocument(null); } this.pdfLinkService.externalLinkEnabled = true; this.store = null; @@ -1905,7 +1905,7 @@ const PDFViewerApplication = { verifier, eventBus: this.eventBus, }); - this.signaturePropertiesManager.loadFromDocument(pdfDocument); + this.signaturePropertiesManager.setDocument(pdfDocument); return true; }, diff --git a/web/digital_signature_properties_manager.js b/web/digital_signature_properties_manager.js index 9de306f4137cc..17f285175e334 100644 --- a/web/digital_signature_properties_manager.js +++ b/web/digital_signature_properties_manager.js @@ -87,8 +87,7 @@ function bannerStateForResults(results) { let worst = "verified"; for (const r of results) { if ( - r && - r.status && + r?.status && STATUS_INFO[r.status].priority > STATUS_INFO[worst].priority ) { worst = r.status; @@ -193,15 +192,6 @@ class SignaturePropertiesManager { // is updated via #updateButtonState(). #needsRender = false; - // Identifies the current `loadFromDocument` call. Each load rotates the - // token; in-flight `#verify()` promises capture the value at start and - // bail on resolve if the manager has since switched to another document. - // Prevents a stale verification for doc A from writing into doc B's - // results map after a fast doc-switch. - #loadToken = null; - - // Held only to ferry per-signature byte payloads from the worker into - // `#verify`. Cleared in `reset()` so we don't pin a closed document. #pdfDocument = null; constructor({ appConfig, verifier, eventBus }) { @@ -258,10 +248,21 @@ class SignaturePropertiesManager { ); } - async loadFromDocument(pdfDocument) { - const token = Symbol("sig-load"); - this.#loadToken = token; + async setDocument(pdfDocument) { + if (this.#pdfDocument) { + this.#signatures = []; + this.#results.clear(); + this.#pendingVerify.clear(); + this.#needsRender = false; + this.#hideButton(); + this.#close(); + this.#updateButtonState(); + } this.#pdfDocument = pdfDocument; + + if (!pdfDocument) { + return; + } this.#signatures = []; this.#results.clear(); this.#pendingVerify.clear(); @@ -275,8 +276,7 @@ class SignaturePropertiesManager { console.warn("getSignatures failed:", ex); signatures = []; } - if (this.#loadToken !== token) { - // A newer document load (or reset) raced ahead — drop this result. + if (pdfDocument !== this.#pdfDocument) { return; } this.#signatures = signatures || []; @@ -305,26 +305,10 @@ class SignaturePropertiesManager { // Kick off verification automatically — the toolbar button reflects the // aggregate state and updates as each signature resolves. for (const sig of this.#signatures) { - this.#verify(sig, token); + this.#verify(sig, pdfDocument); } } - reset() { - // Rotate the token so any in-flight verify from the previous document - // sees a mismatch and bails before mutating state. - this.#loadToken = null; - this.#pdfDocument = null; - this.#signatures = []; - this.#results.clear(); - this.#pendingVerify.clear(); - this.#needsRender = false; - this.#hideButton(); - this.#close(); - this.#updateButtonState(); - } - - // --- internal --- - #showButton() { const root = this.#appConfig.signaturePropertiesButton.parentElement; if (root) { @@ -635,7 +619,7 @@ class SignaturePropertiesManager { return total; } - async #verify(signature, loadToken) { + async #verify(signature, pdfDocument) { if (!this.#verifier || this.#pendingVerify.has(signature.id)) { return; } @@ -648,8 +632,8 @@ class SignaturePropertiesManager { // lazily, one signature at a time, so the bytes never sit in main // thread memory for the document's lifetime. `bytes` goes out of // scope as soon as the verifier returns. - const bytes = await this.#pdfDocument?.getSignatureData(signature.id); - if (this.#loadToken !== loadToken) { + const bytes = await pdfDocument.getSignatureData(signature.id); + if (pdfDocument !== this.#pdfDocument) { return; } if (!bytes) { @@ -667,7 +651,7 @@ class SignaturePropertiesManager { }; } this.#pendingVerify.delete(signature.id); - if (this.#loadToken !== loadToken) { + if (pdfDocument !== this.#pdfDocument) { // The user switched documents while this verify was in flight; the // result belongs to a defunct load and would corrupt the new doc. return;