diff --git a/src/core/obj.js b/src/core/obj.js index 79bd820ce..d8721ba8a 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -731,6 +731,7 @@ class Catalog { getPageDict(pageIndex) { const capability = createPromiseCapability(); const nodesToVisit = [this.catDict.getRaw("Pages")]; + const visitedNodes = new RefSet(); const xref = this.xref, pageKidsCountCache = this.pageKidsCountCache; let count, @@ -747,6 +748,14 @@ class Catalog { currentPageIndex += count; continue; } + // Prevent circular references in the /Pages tree. + if (visitedNodes.has(currentNode)) { + capability.reject( + new FormatError("Pages tree contains circular reference.") + ); + return; + } + visitedNodes.put(currentNode); xref.fetchAsync(currentNode).then(function(obj) { if (isDict(obj, "Page") || (isDict(obj) && !obj.has("Kids"))) { diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 4d6f4e308..df7944553 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -117,6 +117,7 @@ !bug1132849.pdf !issue6894.pdf !issue5804.pdf +!Pages-tree-refs.pdf !ShowText-ShadingPattern.pdf !complex_ttf_font.pdf !issue3694_reduced.pdf diff --git a/test/pdfs/Pages-tree-refs.pdf b/test/pdfs/Pages-tree-refs.pdf new file mode 100644 index 000000000..106de472c --- /dev/null +++ b/test/pdfs/Pages-tree-refs.pdf @@ -0,0 +1,84 @@ +%PDF-1.7 + +1 0 obj + << /Type /Catalog + /Pages 2 0 R + >> +endobj + +2 0 obj + << /Type /Pages + /Kids [6 0 R 3 0 R] + /Count 2 + /MediaBox [0 0 595 842] + >> +endobj + +3 0 obj + << /Type /Pages + /Kids [4 0 R] + /Count 1 + /MediaBox [0 0 595 842] + >> +endobj + +4 0 obj + << /Type /Pages + /Kids [5 0 R] + /Count 1 + /MediaBox [0 0 595 842] + >> +endobj + +5 0 obj + << /Type /Pages + /Kids [3 0 R] + /Count 1 + /MediaBox [0 0 595 842] + >> +endobj + +6 0 obj + << /Type /Page + /Parent 2 0 R + /Resources + << /Font + << /F1 + << /Type /Font + /Subtype /Type1 + /BaseFont /Courier + >> + >> + >> + /Contents [7 0 R] + >> +endobj + +7 0 obj + << /Length 69 >> +stream + BT + /F1 22 Tf + 30 800 Td + (Testcase: 'Pages loop') Tj + ET +endstream +endobj + +xref +0 8 +0000000000 65535 f +0000000010 00000 n +0000000069 00000 n +0000000176 00000 n +0000000277 00000 n +0000000378 00000 n +0000000479 00000 n +0000000744 00000 n +trailer + << /Root 1 0 R + /Size 8 + >> +startxref +866 +%%EOF diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index b06a19458..30a6d30a3 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -546,6 +546,43 @@ describe("api", function() { }) .catch(done.fail); }); + + it("gets page, from /Pages tree with circular reference", function(done) { + const loadingTask = getDocument( + buildGetDocumentParams("Pages-tree-refs.pdf") + ); + + const page1 = loadingTask.promise.then(function(pdfDoc) { + return pdfDoc.getPage(1).then( + function(pdfPage) { + expect(pdfPage instanceof PDFPageProxy).toEqual(true); + expect(pdfPage.ref).toEqual({ num: 6, gen: 0 }); + }, + function(reason) { + throw new Error("shall not fail for valid page"); + } + ); + }); + + const page2 = loadingTask.promise.then(function(pdfDoc) { + return pdfDoc.getPage(2).then( + function(pdfPage) { + throw new Error("shall fail for invalid page"); + }, + function(reason) { + expect(reason instanceof Error).toEqual(true); + expect(reason.message).toEqual( + "Pages tree contains circular reference." + ); + } + ); + }); + + Promise.all([page1, page2]).then(function() { + loadingTask.destroy().then(done); + }, done.fail); + }); + it("gets page index", function(done) { // reference to second page var ref = { num: 17, gen: 0 }; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 7dd696f93..bea9e6d15 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -403,16 +403,20 @@ class PDFPageView { console.error("Must be in new state before drawing"); this.reset(); // Ensure that we reset all state to prevent issues. } + const { div, pdfPage } = this; - if (!this.pdfPage) { + if (!pdfPage) { this.renderingState = RenderingStates.FINISHED; - return Promise.reject(new Error("Page is not loaded")); + + if (this.loadingIconDiv) { + div.removeChild(this.loadingIconDiv); + delete this.loadingIconDiv; + } + return Promise.reject(new Error("pdfPage is not loaded")); } this.renderingState = RenderingStates.RUNNING; - const pdfPage = this.pdfPage; - const div = this.div; // Wrap the canvas so that if it has a CSS transform for high DPI the // overflow will be hidden in Firefox. const canvasWrapper = document.createElement("div"); diff --git a/web/pdf_rendering_queue.js b/web/pdf_rendering_queue.js index f4d9c4f46..444f1cb01 100644 --- a/web/pdf_rendering_queue.js +++ b/web/pdf_rendering_queue.js @@ -164,9 +164,14 @@ class PDFRenderingQueue { break; case RenderingStates.INITIAL: this.highestPriorityPage = view.renderingId; - view.draw().finally(() => { - this.renderHighestPriority(); - }); + view + .draw() + .finally(() => { + this.renderHighestPriority(); + }) + .catch(reason => { + console.error(`renderView: "${reason}"`); + }); break; } return true; diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index a2a5b95ce..785f15bfd 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -295,6 +295,13 @@ class PDFThumbnailView { console.error("Must be in new state before drawing"); return Promise.resolve(undefined); } + const { pdfPage } = this; + + if (!pdfPage) { + this.renderingState = RenderingStates.FINISHED; + return Promise.reject(new Error("pdfPage is not loaded")); + } + this.renderingState = RenderingStates.RUNNING; const renderCapability = createPromiseCapability(); @@ -339,7 +346,7 @@ class PDFThumbnailView { canvasContext: ctx, viewport: drawViewport, }; - const renderTask = (this.renderTask = this.pdfPage.render(renderContext)); + const renderTask = (this.renderTask = pdfPage.render(renderContext)); renderTask.onContinue = renderContinueCallback; renderTask.promise.then(