Skip to content

Commit fc1a152

Browse files
committed
Provide a NullUnlimitedList implementation to prevent OOM exception while opening document with enormous page count
DEVSIX-7793
1 parent 29c6e11 commit fc1a152

File tree

5 files changed

+167
-23
lines changed

5 files changed

+167
-23
lines changed

kernel/src/main/java/com/itextpdf/kernel/pdf/PdfPagesTree.java

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,20 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.kernel.pdf;
2424

25-
import com.itextpdf.io.logs.IoLogMessageConstant;
2625
import com.itextpdf.commons.utils.MessageFormatUtil;
27-
import com.itextpdf.kernel.exceptions.PdfException;
26+
import com.itextpdf.io.logs.IoLogMessageConstant;
2827
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
28+
import com.itextpdf.kernel.exceptions.PdfException;
2929

30+
import java.util.ArrayList;
31+
import java.util.HashMap;
3032
import java.util.HashSet;
33+
import java.util.List;
34+
import java.util.Map;
3135
import java.util.Set;
3236
import org.slf4j.Logger;
3337
import org.slf4j.LoggerFactory;
3438

35-
import java.util.ArrayList;
36-
import java.util.List;
37-
3839
/**
3940
* Algorithm for construction {@link PdfPages} tree
4041
*/
@@ -44,9 +45,9 @@ class PdfPagesTree {
4445

4546
private final int leafSize = DEFAULT_LEAF_SIZE;
4647

47-
private List<PdfIndirectReference> pageRefs;
48+
private NullUnlimitedList<PdfIndirectReference> pageRefs;
4849
private List<PdfPages> parents;
49-
private List<PdfPage> pages;
50+
private NullUnlimitedList<PdfPage> pages;
5051
private PdfDocument document;
5152
private boolean generated = false;
5253
private PdfPages root;
@@ -60,9 +61,9 @@ class PdfPagesTree {
6061
*/
6162
public PdfPagesTree(PdfCatalog pdfCatalog) {
6263
this.document = pdfCatalog.getDocument();
63-
this.pageRefs = new ArrayList<>();
64+
this.pageRefs = new NullUnlimitedList<>();
6465
this.parents = new ArrayList<>();
65-
this.pages = new ArrayList<>();
66+
this.pages = new NullUnlimitedList<>();
6667
if (pdfCatalog.getPdfObject().containsKey(PdfName.Pages)) {
6768
PdfDictionary pages = pdfCatalog.getPdfObject().getAsDictionary(PdfName.Pages);
6869
if (pages == null) {
@@ -451,10 +452,9 @@ private void loadPage(int pageNum, Set<PdfIndirectReference> processedParents) {
451452
} else {
452453
int from = parent.getFrom();
453454

454-
// Possible exception in case kids.getSize() < parent.getCount().
455-
// In any case parent.getCount() has higher priority.
456455
// NOTE optimization? when we already found needed index
457-
for (int i = 0; i < parent.getCount(); i++) {
456+
final int pageCount = Math.min(parent.getCount(), kids.size());
457+
for (int i = 0; i < pageCount; i++) {
458458
PdfObject kid = kids.get(i, false);
459459
if (kid instanceof PdfIndirectReference) {
460460
pageRefs.set(from + i, (PdfIndirectReference) kid);
@@ -512,4 +512,99 @@ private void correctPdfPagesFromProperty(int index, int correction) {
512512
}
513513
}
514514
}
515+
516+
/**
517+
* The class represents a list which allows null elements, but doesn't allocate a memory for them, in the rest of
518+
* cases it behaves like usual {@link ArrayList} and should have the same complexity (because keys are unique
519+
* integers, so collisions are impossible). Class doesn't implement {@code List} interface because it provides
520+
* only methods which are in use in {@link PdfPagesTree} class.
521+
*
522+
* @param <T> elements of the list
523+
*/
524+
static final class NullUnlimitedList<T> {
525+
private final Map<Integer, T> map = new HashMap<>();
526+
private int size = 0;
527+
528+
// O(1)
529+
public void add(T element) {
530+
if (element == null) {
531+
size++;
532+
return;
533+
}
534+
map.put(size++, element);
535+
}
536+
537+
// In worth scenario O(n^2) but it is mostly impossible because keys shouldn't have
538+
// collisions at all (they are integers). So in average should be O(n).
539+
public void add(int index, T element) {
540+
if (index < 0 || index > size) {
541+
return;
542+
}
543+
size++;
544+
// Shifts the element currently at that position (if any) and any
545+
// subsequent elements to the right (adds one to their indices).
546+
T previous = map.get(index);
547+
for (int i = index + 1; i < size; i++) {
548+
T currentToAdd = previous;
549+
previous = map.get(i);
550+
this.set(i, currentToAdd);
551+
}
552+
553+
this.set(index, element);
554+
}
555+
556+
// average O(1), worth O(n) (mostly impossible in case when keys are integers)
557+
public T get(int index) {
558+
return map.get(index);
559+
}
560+
561+
// average O(1), worth O(n) (mostly impossible in case when keys are integers)
562+
public void set(int index, T element) {
563+
if (element == null) {
564+
map.remove(index);
565+
} else {
566+
map.put(index, element);
567+
}
568+
}
569+
570+
// O(n)
571+
public int indexOf(T element) {
572+
if (element == null) {
573+
for (int i = 0; i < size; i++) {
574+
if (!map.containsKey(i)) {
575+
return i;
576+
}
577+
}
578+
return -1;
579+
}
580+
for (Map.Entry<Integer, T> entry : map.entrySet()) {
581+
if (element.equals(entry.getValue())) {
582+
return entry.getKey();
583+
}
584+
}
585+
return -1;
586+
}
587+
588+
// In worth scenario O(n^2) but it is mostly impossible because keys shouldn't have
589+
// collisions at all (they are integers). So in average should be O(n).
590+
public void remove(int index) {
591+
if (index < 0 || index >= size) {
592+
return;
593+
}
594+
map.remove(index);
595+
// Shifts any subsequent elements to the left (subtracts one from their indices).
596+
T previous = map.get(size - 1);
597+
for (int i = size - 2; i >= index; i--) {
598+
T current = previous;
599+
previous = map.get(i);
600+
this.set(i, current);
601+
}
602+
map.remove(--size);
603+
}
604+
605+
// O(1)
606+
public int size() {
607+
return size;
608+
}
609+
}
515610
}

kernel/src/test/java/com/itextpdf/kernel/pdf/PdfPagesTest.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.kernel.pdf;
2424

25-
import com.itextpdf.io.logs.IoLogMessageConstant;
25+
import com.itextpdf.commons.utils.MessageFormatUtil;
2626
import com.itextpdf.io.image.ImageDataFactory;
27+
import com.itextpdf.io.logs.IoLogMessageConstant;
2728
import com.itextpdf.io.source.RandomAccessSourceFactory;
28-
import com.itextpdf.commons.utils.MessageFormatUtil;
29-
import com.itextpdf.kernel.exceptions.PdfException;
3029
import com.itextpdf.kernel.colors.ColorConstants;
3130
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
31+
import com.itextpdf.kernel.exceptions.PdfException;
3232
import com.itextpdf.kernel.geom.PageSize;
3333
import com.itextpdf.kernel.geom.Rectangle;
3434
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
@@ -42,10 +42,6 @@ This file is part of the iText (R) project.
4242
import com.itextpdf.test.annotations.LogMessage;
4343
import com.itextpdf.test.annotations.LogMessages;
4444
import com.itextpdf.test.annotations.type.IntegrationTest;
45-
import org.junit.Assert;
46-
import org.junit.BeforeClass;
47-
import org.junit.Test;
48-
import org.junit.experimental.categories.Category;
4945

5046
import java.io.ByteArrayOutputStream;
5147
import java.io.IOException;
@@ -55,6 +51,10 @@ This file is part of the iText (R) project.
5551
import java.util.List;
5652
import java.util.Random;
5753
import java.util.Set;
54+
import org.junit.Assert;
55+
import org.junit.BeforeClass;
56+
import org.junit.Test;
57+
import org.junit.experimental.categories.Category;
5858

5959
@Category(IntegrationTest.class)
6060
public class PdfPagesTest extends ExtendedITextTest {
@@ -67,6 +67,25 @@ public static void setup() {
6767
createDestinationFolder(DESTINATION_FOLDER);
6868
}
6969

70+
@Test
71+
public void hugeNumberOfPagesWithOnePageTest() throws IOException {
72+
PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "hugeNumberOfPagesWithOnePage.pdf"),
73+
new PdfWriter(new ByteArrayOutputStream()));
74+
PdfPage page = new PdfPage(pdfDoc, pdfDoc.getDefaultPageSize());
75+
AssertUtil.doesNotThrow(() -> pdfDoc.addPage(1, page));
76+
}
77+
78+
@Test
79+
public void countDontCorrespondToRealTest() throws IOException {
80+
PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "countDontCorrespondToReal.pdf"),
81+
new PdfWriter(new ByteArrayOutputStream()));
82+
PdfPage page = new PdfPage(pdfDoc, pdfDoc.getDefaultPageSize());
83+
AssertUtil.doesNotThrow(() -> pdfDoc.addPage(1, page));
84+
85+
// we don't expect that Count will be different from real number of pages
86+
Assert.assertThrows(NullPointerException.class, () -> pdfDoc.close());
87+
}
88+
7089
@Test
7190
public void simplePagesTest() throws IOException {
7291
String filename = "simplePagesTest.pdf";

kernel/src/test/java/com/itextpdf/kernel/pdf/PdfPagesTreeTest.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,11 @@ This file is part of the iText (R) project.
2323
package com.itextpdf.kernel.pdf;
2424

2525
import com.itextpdf.io.source.ByteArrayOutputStream;
26-
import com.itextpdf.kernel.events.PdfDocumentEvent;
27-
import com.itextpdf.kernel.pdf.layer.PdfLayer;
28-
import com.itextpdf.kernel.utils.CompareTool;
26+
import com.itextpdf.kernel.pdf.PdfPagesTree.NullUnlimitedList;
2927
import com.itextpdf.test.AssertUtil;
3028
import com.itextpdf.test.ExtendedITextTest;
3129
import com.itextpdf.test.annotations.type.UnitTest;
3230

33-
import java.io.IOException;
3431
import org.junit.Assert;
3532
import org.junit.Test;
3633
import org.junit.experimental.categories.Category;
@@ -42,4 +39,37 @@ public void generateTreeDocHasNoPagesTest() {
4239
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
4340
AssertUtil.doesNotThrow(() -> pdfDoc.close());
4441
}
42+
43+
@Test
44+
public void nullUnlimitedListAddTest() {
45+
NullUnlimitedList<String> list = new NullUnlimitedList<>();
46+
list.add("hey");
47+
list.add("bye");
48+
Assert.assertEquals(2, list.size());
49+
list.add(-1, "hello");
50+
list.add(3, "goodbye");
51+
Assert.assertEquals(2, list.size());
52+
}
53+
54+
@Test
55+
public void nullUnlimitedListIndexOfTest() {
56+
NullUnlimitedList<String> list = new NullUnlimitedList<>();
57+
list.add("hey");
58+
list.add(null);
59+
list.add("bye");
60+
list.add(null);
61+
Assert.assertEquals(4, list.size());
62+
Assert.assertEquals(1, list.indexOf(null));
63+
}
64+
65+
@Test
66+
public void nullUnlimitedListRemoveTest() {
67+
NullUnlimitedList<String> list = new NullUnlimitedList<>();
68+
list.add("hey");
69+
list.add("bye");
70+
Assert.assertEquals(2, list.size());
71+
list.remove(-1);
72+
list.remove(2);
73+
Assert.assertEquals(2, list.size());
74+
}
4575
}

0 commit comments

Comments
 (0)