diff --git a/src/main/java/org/verapdf/cos/COSDocument.java b/src/main/java/org/verapdf/cos/COSDocument.java index 17ed9b43..5eaeebfe 100644 --- a/src/main/java/org/verapdf/cos/COSDocument.java +++ b/src/main/java/org/verapdf/cos/COSDocument.java @@ -25,6 +25,7 @@ import org.verapdf.cos.visitor.Writer; import org.verapdf.cos.xref.COSXRefTable; import org.verapdf.exceptions.LoopedException; +import org.verapdf.exceptions.ResourceLimitExceededException; import org.verapdf.exceptions.VeraPDFParserException; import org.verapdf.io.IReader; import org.verapdf.io.InternalInputStream; @@ -143,9 +144,10 @@ public void setHeader(String header) { * Sets an upper bound on the number of indirect objects the document may declare in its * cross-reference table. Enumerating the objects materialises one entry per key, so a document that * declares an extreme number of objects (a small compressed input can, via an object stream) can - * exhaust the heap; with a bound set it is rejected with a {@link VeraPDFParserException} before the - * objects are materialised, instead of failing with an OutOfMemoryError. A negative value (the - * default) removes the bound, keeping the historical behaviour. + * exhaust the heap; with a bound set it is rejected with a {@link ResourceLimitExceededException} + * (a {@link VeraPDFParserException} subclass, so existing catches keep working) before the objects + * are materialised, instead of failing with an OutOfMemoryError. A negative value (the default) + * removes the bound, keeping the historical behaviour. * * @param max maximum number of indirect objects, or a negative value for no bound */ @@ -164,7 +166,7 @@ private void checkObjectCountLimit() { if (maxNumberOfObjects >= 0) { int declared = this.xref.getAllKeys().size(); if (declared > maxNumberOfObjects) { - throw new VeraPDFParserException("Number of indirect objects (" + declared + throw new ResourceLimitExceededException("Number of indirect objects (" + declared + ") exceeds the configured maximum of " + maxNumberOfObjects); } } diff --git a/src/main/java/org/verapdf/exceptions/ResourceLimitExceededException.java b/src/main/java/org/verapdf/exceptions/ResourceLimitExceededException.java new file mode 100644 index 00000000..48f834d2 --- /dev/null +++ b/src/main/java/org/verapdf/exceptions/ResourceLimitExceededException.java @@ -0,0 +1,49 @@ +/* + * This file is part of veraPDF Parser, a module of the veraPDF project. + * Copyright (c) 2015-2026, veraPDF Consortium + * All rights reserved. + * + * veraPDF Parser is free software: you can redistribute it and/or modify + * it under the terms of either: + * + * The GNU General public license GPLv3+. + * You should have received a copy of the GNU General Public License + * along with veraPDF Parser as the LICENSE.GPL file in the root of the source + * tree. If not, see http://www.gnu.org/licenses/ or + * https://www.gnu.org/licenses/gpl-3.0.en.html. + * + * The Mozilla Public License MPLv2+. + * You should have received a copy of the Mozilla Public License along with + * veraPDF Parser as the LICENSE.MPL file in the root of the source tree. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * http://mozilla.org/MPL/2.0/. + */ +package org.verapdf.exceptions; + +/** + * Thrown when parsing is aborted because a configured resource limit is exceeded, as opposed to the + * input being malformed. It is a subclass of {@link VeraPDFParserException}, so existing callers that + * catch the parser exception keep working unchanged; a caller that needs to tell a deliberate + * resource-limit abort (for example the caps set through {@code SeekableInputStream.setMaxStreamSize} + * or {@code COSDocument.setMaxNumberOfObjects}) apart from a genuine parse error can catch this type + * instead of matching on the message text. + * + * @author veraPDF Consortium + */ +public class ResourceLimitExceededException extends VeraPDFParserException { + + public ResourceLimitExceededException() { + } + + public ResourceLimitExceededException(String message) { + super(message); + } + + public ResourceLimitExceededException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceLimitExceededException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/verapdf/io/InternalInputStream.java b/src/main/java/org/verapdf/io/InternalInputStream.java index f76e1707..53b4dd93 100644 --- a/src/main/java/org/verapdf/io/InternalInputStream.java +++ b/src/main/java/org/verapdf/io/InternalInputStream.java @@ -22,6 +22,7 @@ import org.verapdf.as.filters.io.ASBufferedInFilter; import org.verapdf.as.io.ASInputStream; +import org.verapdf.exceptions.ResourceLimitExceededException; import org.verapdf.exceptions.VeraPDFParserException; import org.verapdf.tools.IntReference; @@ -284,7 +285,7 @@ public long getStreamLength() throws IOException { private static File createTempFile(byte[] alreadyRead, InputStream input, Integer maxStreamSize) throws IOException { if (maxStreamSize != null && alreadyRead.length > maxStreamSize) { - throw new VeraPDFParserException("Maximum allowed stream size exceeded"); + throw new ResourceLimitExceededException("Maximum allowed stream size exceeded"); } File tmpFile = TempFileHandler.createTempFile("tmp_pdf_file", ".pdf"); try (FileOutputStream output = new FileOutputStream(tmpFile)) { @@ -297,7 +298,7 @@ private static File createTempFile(byte[] alreadyRead, InputStream input, Intege while ((n = input.read(buffer, 0, ASBufferedInFilter.BF_BUFFER_SIZE)) != -1) { totalRead += n; if (maxStreamSize != null && totalRead > maxStreamSize) { - throw new VeraPDFParserException("Maximum allowed stream size exceeded"); + throw new ResourceLimitExceededException("Maximum allowed stream size exceeded"); } output.write(buffer, 0, n); } diff --git a/src/main/java/org/verapdf/io/SeekableInputStream.java b/src/main/java/org/verapdf/io/SeekableInputStream.java index e6f655c2..0e00b1c4 100644 --- a/src/main/java/org/verapdf/io/SeekableInputStream.java +++ b/src/main/java/org/verapdf/io/SeekableInputStream.java @@ -23,7 +23,7 @@ import org.verapdf.as.filters.io.ASBufferedInFilter; import org.verapdf.as.io.ASInputStream; import org.verapdf.as.io.ASMemoryInStream; -import org.verapdf.exceptions.VeraPDFParserException; +import org.verapdf.exceptions.ResourceLimitExceededException; import org.verapdf.parser.BaseParserInputStream; import java.io.IOException; @@ -44,7 +44,7 @@ public abstract class SeekableInputStream extends ASInputStream implements BaseP * Optional hard cap, in bytes, on the size of a single stream that is spilled to a temporary file * ({@code null} means no cap). It guards against a small input whose decoded content expands without * bound (a "decompression bomb"): a stream that exceeds the cap is rejected with a - * {@link org.verapdf.exceptions.VeraPDFParserException} instead of being written out in full. The + * {@link org.verapdf.exceptions.ResourceLimitExceededException} instead of being written out in full. The * limit is wired into {@link #getSeekableStream(InputStream)}, which is the path taken by decoded * object streams and other non-seekable input. Default {@code null}, so behaviour is unchanged. */ @@ -237,7 +237,7 @@ public static SeekableInputStream getSeekableStream(InputStream stream, Integer totalRead += read; } if (maxStreamSize != null && totalRead > maxStreamSize) { - throw new VeraPDFParserException("Maximum allowed stream size exceeded"); + throw new ResourceLimitExceededException("Maximum allowed stream size exceeded"); } return InternalInputStream.createConcatenated(buffer, stream, maxStreamSize); } diff --git a/src/test/java/org/verapdf/cos/COSDocumentObjectLimitTest.java b/src/test/java/org/verapdf/cos/COSDocumentObjectLimitTest.java new file mode 100644 index 00000000..3921ae1f --- /dev/null +++ b/src/test/java/org/verapdf/cos/COSDocumentObjectLimitTest.java @@ -0,0 +1,75 @@ +/* + * This file is part of veraPDF Parser, a module of the veraPDF project. + * Copyright (c) 2015-2026, veraPDF Consortium + * All rights reserved. + * + * veraPDF Parser is free software: you can redistribute it and/or modify + * it under the terms of either: + * + * The GNU General public license GPLv3+. + * You should have received a copy of the GNU General Public License + * along with veraPDF Parser as the LICENSE.GPL file in the root of the source + * tree. If not, see http://www.gnu.org/licenses/ or + * https://www.gnu.org/licenses/gpl-3.0.en.html. + * + * The Mozilla Public License MPLv2+. + * You should have received a copy of the Mozilla Public License along with + * veraPDF Parser as the LICENSE.MPL file in the root of the source tree. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * http://mozilla.org/MPL/2.0/. + */ +package org.verapdf.cos; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.verapdf.as.ASAtom; +import org.verapdf.exceptions.ResourceLimitExceededException; +import org.verapdf.pd.PDDocument; + +import java.io.IOException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +public class COSDocumentObjectLimitTest { + + private static final String FILE_PATH = "src/test/resources/org/verapdf/cos/filters/validDocument.pdf"; + + private int previousLimit; + + @Before + public void rememberLimit() { + previousLimit = COSDocument.getMaxNumberOfObjects(); + } + + @After + public void restoreLimit() { + COSDocument.setMaxNumberOfObjects(previousLimit); + } + + @Test + public void shouldEnumerateObjectsWhenTheLimitIsNotExceeded() throws IOException { + PDDocument document = new PDDocument(FILE_PATH); + try { + COSDocument.setMaxNumberOfObjects(Integer.MAX_VALUE); + assertFalse(document.getDocument().getObjects().isEmpty()); + } finally { + document.close(); + } + } + + @Test + public void shouldThrowResourceLimitExceededExceptionWhenTheLimitIsExceeded() throws IOException { + PDDocument document = new PDDocument(FILE_PATH); + try { + COSDocument cosDocument = document.getDocument(); + COSDocument.setMaxNumberOfObjects(1); + assertThrows(ResourceLimitExceededException.class, cosDocument::getObjects); + assertThrows(ResourceLimitExceededException.class, cosDocument::getObjectsMap); + assertThrows(ResourceLimitExceededException.class, () -> cosDocument.getObjectsByType(ASAtom.PAGE)); + } finally { + document.close(); + } + } +} diff --git a/src/test/java/org/verapdf/io/InternalInputStreamTest.java b/src/test/java/org/verapdf/io/InternalInputStreamTest.java index efd1a7ed..c9230557 100644 --- a/src/test/java/org/verapdf/io/InternalInputStreamTest.java +++ b/src/test/java/org/verapdf/io/InternalInputStreamTest.java @@ -35,7 +35,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.verapdf.exceptions.VeraPDFParserException; +import org.verapdf.exceptions.ResourceLimitExceededException; public class InternalInputStreamTest { @Rule @@ -273,8 +273,8 @@ public void shouldNotThrowExceptionWhenSumOfInputsIsNotLargerThanMaxSize() throw public void shouldThrowExceptionWhenAlreadyReadIsLargerThanMaxSizeAndStreamIsEmpty() { byte[] alreadyRead = "abc".getBytes(); ByteArrayInputStream stream = new ByteArrayInputStream("".getBytes()); - VeraPDFParserException thrown = assertThrows( - VeraPDFParserException.class, + ResourceLimitExceededException thrown = assertThrows( + ResourceLimitExceededException.class, () -> InternalInputStream.createConcatenated(alreadyRead, stream, 2) ); assertTrue(thrown.getMessage().contains("size exceeded")); @@ -284,8 +284,8 @@ public void shouldThrowExceptionWhenAlreadyReadIsLargerThanMaxSizeAndStreamIsEmp public void shouldThrowExceptionWhenAlreadyReadIsLargerThanMaxSizeAndStreamIsNotEmpty() { byte[] alreadyRead = "abc".getBytes(); ByteArrayInputStream stream = new ByteArrayInputStream("1".getBytes()); - VeraPDFParserException thrown = assertThrows( - VeraPDFParserException.class, + ResourceLimitExceededException thrown = assertThrows( + ResourceLimitExceededException.class, () -> InternalInputStream.createConcatenated(alreadyRead, stream, 2) ); assertTrue(thrown.getMessage().contains("size exceeded")); @@ -295,8 +295,8 @@ public void shouldThrowExceptionWhenAlreadyReadIsLargerThanMaxSizeAndStreamIsNot public void shouldThrowExceptionWhenStreamIsLargerThanMaxSizeAndAlreadyReadIsEmpty() { byte[] alreadyRead = "".getBytes(); ByteArrayInputStream stream = new ByteArrayInputStream("abc".getBytes()); - VeraPDFParserException thrown = assertThrows( - VeraPDFParserException.class, + ResourceLimitExceededException thrown = assertThrows( + ResourceLimitExceededException.class, () -> InternalInputStream.createConcatenated(alreadyRead, stream, 2) ); assertTrue(thrown.getMessage().contains("size exceeded")); @@ -306,8 +306,8 @@ public void shouldThrowExceptionWhenStreamIsLargerThanMaxSizeAndAlreadyReadIsEmp public void shouldThrowExceptionWhenStreamIsLargerThanMaxSizeAndAlreadyReadIsNotEmpty() { byte[] alreadyRead = "1".getBytes(); ByteArrayInputStream stream = new ByteArrayInputStream("abc".getBytes()); - VeraPDFParserException thrown = assertThrows( - VeraPDFParserException.class, + ResourceLimitExceededException thrown = assertThrows( + ResourceLimitExceededException.class, () -> InternalInputStream.createConcatenated(alreadyRead, stream, 2) ); assertTrue(thrown.getMessage().contains("size exceeded")); @@ -317,8 +317,8 @@ public void shouldThrowExceptionWhenStreamIsLargerThanMaxSizeAndAlreadyReadIsNot public void shouldThrowExceptionWhenSumOfAlreadyReadAndStreamIsLargerThanMaxSize() { byte[] alreadyRead = "ab".getBytes(); ByteArrayInputStream stream = new ByteArrayInputStream("cd".getBytes()); - VeraPDFParserException thrown = assertThrows( - VeraPDFParserException.class, + ResourceLimitExceededException thrown = assertThrows( + ResourceLimitExceededException.class, () -> InternalInputStream.createConcatenated(alreadyRead, stream, 2) ); assertTrue(thrown.getMessage().contains("size exceeded")); diff --git a/src/test/java/org/verapdf/io/SeekableInputStreamParametricTest.java b/src/test/java/org/verapdf/io/SeekableInputStreamParametricTest.java index 97634b58..f0e8f0ca 100644 --- a/src/test/java/org/verapdf/io/SeekableInputStreamParametricTest.java +++ b/src/test/java/org/verapdf/io/SeekableInputStreamParametricTest.java @@ -26,7 +26,7 @@ import org.junit.runners.Parameterized.Parameters; import org.verapdf.as.io.ASInputStream; import org.verapdf.as.io.ASMemoryInStream; -import org.verapdf.exceptions.VeraPDFParserException; +import org.verapdf.exceptions.ResourceLimitExceededException; import java.io.ByteArrayInputStream; import java.io.File; @@ -398,7 +398,7 @@ public void shouldNotThrowExceptionsAboutExceedingMaxSizeWhenNotExeedingMaxSize( @Test public void shouldThrowExceptionsAboutExceedingMaxSizeWhenExeedingMaxSize() { - assertThrows(VeraPDFParserException.class, () -> SeekableInputStream.getSeekableStream(input, size - 1)); + assertThrows(ResourceLimitExceededException.class, () -> SeekableInputStream.getSeekableStream(input, size - 1)); } @Test