diff --git a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java index 315c5708213..167c6b1193d 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java @@ -16,6 +16,9 @@ */ package org.apache.pdfbox.cos; +import java.util.ArrayDeque; +import java.util.Deque; + /** * A {@link COSUpdateState} instance manages update states for a {@link COSUpdateInfo}. Such states are used to create * a {@link COSIncrement} for the incremental saving of a {@link COSDocument}. @@ -117,51 +120,71 @@ public void setOriginDocumentState(COSDocumentState originDocumentState) */ private void setOriginDocumentState(COSDocumentState originDocumentState, boolean dereferencing) { - if(this.originDocumentState != null || originDocumentState == null) + if (originDocumentState == null) { return; } - this.originDocumentState = originDocumentState; - if(!dereferencing) + // The structure is traversed with an explicit stack instead of recursion, as it may be + // nested deeper than the call stack allows (PDFBOX-6200). + Deque pending = new ArrayDeque<>(); + pending.push(this); + while (!pending.isEmpty()) { - update(); + COSUpdateState state = pending.pop(); + if (state.originDocumentState != null) + { + continue; + } + state.originDocumentState = originDocumentState; + if (!dereferencing) + { + state.update(); + } + state.pushSubstructures(pending); } - - if(updateInfo instanceof COSDictionary) + } + + /** + * Pushes the {@link COSUpdateState}s of the substructures directly contained by the managed + * {@link #updateInfo} onto the given stack. + * + * @param pending The stack of {@link COSUpdateState}s that still have to be processed. + */ + private void pushSubstructures(Deque pending) + { + if (updateInfo instanceof COSDictionary) { COSDictionary dictionary = (COSDictionary) updateInfo; - for(COSBase entry : dictionary.getValues()) + for (COSBase entry : dictionary.getValues()) { - if (entry instanceof COSUpdateInfo) - { - ((COSUpdateInfo) entry).getUpdateState().setOriginDocumentState(originDocumentState, dereferencing); - } + pushUpdateState(pending, entry); } } - else if(updateInfo instanceof COSArray) + else if (updateInfo instanceof COSArray) { COSArray array = (COSArray) updateInfo; - for(COSBase entry : array) + for (COSBase entry : array) { - if (entry instanceof COSUpdateInfo) - { - ((COSUpdateInfo) entry).getUpdateState().setOriginDocumentState(originDocumentState, dereferencing); - } + pushUpdateState(pending, entry); } } - else if(updateInfo instanceof COSObject) + else if (updateInfo instanceof COSObject) { COSObject object = (COSObject) updateInfo; if (object.isDereferenced()) { - COSBase reference = object.getObject(); - if (reference instanceof COSUpdateInfo) - { - ((COSUpdateInfo) reference).getUpdateState().setOriginDocumentState(originDocumentState, dereferencing); - } + pushUpdateState(pending, object.getObject()); } } } + + private static void pushUpdateState(Deque pending, COSBase base) + { + if (base instanceof COSUpdateInfo) + { + pending.push(((COSUpdateInfo) base).getUpdateState()); + } + } /** *

diff --git a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java index 37b477d9836..054eb615680 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java @@ -17,6 +17,7 @@ package org.apache.pdfbox.cos; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -58,4 +59,46 @@ void testIsSetNeedToBeUpdate() assertFalse(testCOSObject.isNeedToBeUpdated()); } + + /** + * PDFBOX-6200: linking a document state to a deeply nested structure must not overflow the + * stack. + */ + @Test + void testSetOriginDocumentStateOfDeeplyNestedStructure() + { + COSDocumentState origin = new COSDocumentState(); + origin.setParsing(false); + + // a chain of dictionaries, arrays and dereferenced indirect objects, built from the end + COSDictionary deepest = new COSDictionary(); + COSBase current = deepest; + for (int i = 0; i < 50_000; i++) + { + switch (i % 3) + { + case 0: + COSDictionary dictionary = new COSDictionary(); + dictionary.setItem(COSName.getPDFName("Next"), current); + current = dictionary; + break; + case 1: + COSArray array = new COSArray(); + array.add(current); + current = array; + break; + default: + current = new COSObject(current); + break; + } + } + COSUpdateInfo root = (COSUpdateInfo) current; + + root.getUpdateState().setOriginDocumentState(origin); + + assertSame(origin, deepest.getUpdateState().getOriginDocumentState()); + assertTrue(root.isNeedToBeUpdated()); + assertTrue(deepest.isNeedToBeUpdated()); + } + }