diff --git a/src/main/java/org/verapdf/cos/COSDocument.java b/src/main/java/org/verapdf/cos/COSDocument.java index f373e0a2..078040bb 100644 --- a/src/main/java/org/verapdf/cos/COSDocument.java +++ b/src/main/java/org/verapdf/cos/COSDocument.java @@ -30,6 +30,7 @@ import org.verapdf.io.InternalInputStream; import org.verapdf.io.Reader; import org.verapdf.io.SeekableInputStream; +import org.verapdf.io.TempFileHandler; import org.verapdf.pd.PDDocument; import org.verapdf.pd.encryption.StandardSecurityHandler; import org.verapdf.tools.resource.ASFileStreamCloser; @@ -346,7 +347,7 @@ public void saveAs(final Writer writer) { public void saveTo(final OutputStream stream) { File temp = null; try { - temp = File.createTempFile("tmp_pdf_file", ".pdf"); + temp = TempFileHandler.createTempFile("tmp_pdf_file", ".pdf"); Writer pdfWriter = new Writer(this, temp.getAbsolutePath(), this.getPDFSource().getStreamLength()); pdfWriter.writeIncrementalUpdate(changedObjects, addedObjects); diff --git a/src/main/java/org/verapdf/io/InternalInputStream.java b/src/main/java/org/verapdf/io/InternalInputStream.java index 697f95a5..f76e1707 100644 --- a/src/main/java/org/verapdf/io/InternalInputStream.java +++ b/src/main/java/org/verapdf/io/InternalInputStream.java @@ -286,7 +286,7 @@ private static File createTempFile(byte[] alreadyRead, InputStream input, Intege if (maxStreamSize != null && alreadyRead.length > maxStreamSize) { throw new VeraPDFParserException("Maximum allowed stream size exceeded"); } - File tmpFile = File.createTempFile("tmp_pdf_file", ".pdf"); + File tmpFile = TempFileHandler.createTempFile("tmp_pdf_file", ".pdf"); try (FileOutputStream output = new FileOutputStream(tmpFile)) { output.write(alreadyRead); int totalRead = alreadyRead.length; diff --git a/src/main/java/org/verapdf/io/InternalOutputStream.java b/src/main/java/org/verapdf/io/InternalOutputStream.java index e692717e..478f71d2 100644 --- a/src/main/java/org/verapdf/io/InternalOutputStream.java +++ b/src/main/java/org/verapdf/io/InternalOutputStream.java @@ -44,7 +44,7 @@ public class InternalOutputStream implements ASOutputStream, Closeable { * @throws IOException */ public static InternalOutputStream getInternalOutputStream() throws IOException { - File tempFile = File.createTempFile("tmp_pdf_file", ".pdf"); + File tempFile = TempFileHandler.createTempFile("tmp_pdf_file", ".pdf"); return new InternalOutputStream(tempFile); } diff --git a/src/main/java/org/verapdf/io/SeekableInputStream.java b/src/main/java/org/verapdf/io/SeekableInputStream.java index 94bed378..92dfe9f2 100644 --- a/src/main/java/org/verapdf/io/SeekableInputStream.java +++ b/src/main/java/org/verapdf/io/SeekableInputStream.java @@ -40,6 +40,29 @@ public abstract class SeekableInputStream extends ASInputStream implements BaseP private static final int MAX_BUFFER_SIZE = 10240; + /** + * Configurable in-memory buffer threshold: input up to this size is kept in memory, larger + * input is spilled to a temporary file. Defaults to {@link #MAX_BUFFER_SIZE}. A larger value + * trades memory for fewer temporary files; a smaller value does the opposite. + */ + private static volatile int maxBufferSize = MAX_BUFFER_SIZE; + + /** + * Sets the in-memory buffer threshold in bytes. Values below 1 restore the default. + * + * @param bytes buffer threshold in bytes, or a non-positive value for the default + */ + public static void setMaxBufferSize(int bytes) { + maxBufferSize = bytes > 0 ? bytes : MAX_BUFFER_SIZE; + } + + /** + * @return the current in-memory buffer threshold in bytes + */ + public static int getMaxBufferSize() { + return maxBufferSize; + } + /** * Goes to a particular byte in stream. * @@ -177,7 +200,8 @@ public static SeekableInputStream getSeekableStream(InputStream stream, Integer int totalRead = 0; byte[] buffer = new byte[0]; byte[] temp = new byte[ASBufferedInFilter.BF_BUFFER_SIZE]; - int maximumSize = maxStreamSize == null ? MAX_BUFFER_SIZE : Math.min(MAX_BUFFER_SIZE, maxStreamSize + 1); + int bufferThreshold = maxBufferSize; + int maximumSize = maxStreamSize == null ? bufferThreshold : Math.min(bufferThreshold, maxStreamSize + 1); while (totalRead < maximumSize) { int read = stream.read(temp); if (read == -1) { diff --git a/src/main/java/org/verapdf/io/TempFileHandler.java b/src/main/java/org/verapdf/io/TempFileHandler.java new file mode 100644 index 00000000..6eb6bb7f --- /dev/null +++ b/src/main/java/org/verapdf/io/TempFileHandler.java @@ -0,0 +1,112 @@ +/* + * 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.io; + +import java.io.File; +import java.io.IOException; + +/** + * Central creation point for the temporary files that the parser writes while turning non-seekable + * input (embedded font programs, CMaps, decoded object streams, incremental-update output) into + * seekable data. + * + *

By default this behaves exactly like {@link File#createTempFile(String, String)} and writes into + * the JVM temporary directory ({@code java.io.tmpdir}). A caller that needs the temporary files in a + * specific directory - for example one directory per worker so files stay inside an isolated working + * area and can be removed deterministically - can configure a target directory. Two levels are offered: + * a process-wide default and a per-thread override that takes precedence. Both are optional; when + * neither is set the historical behaviour is preserved, so this change is backward compatible.

+ * + *

The per-thread override is the natural fit for a server that validates one document per worker + * thread: set it before parsing, {@link #clearTempDirectory() clear} it afterwards.

+ */ +public final class TempFileHandler { + + private static volatile File defaultTempDirectory; + + private static final ThreadLocal TEMP_DIRECTORY = new ThreadLocal<>(); + + private TempFileHandler() { + } + + /** + * Sets the process-wide default directory for parser temporary files. {@code null} restores the + * JVM default ({@code java.io.tmpdir}). + * + * @param directory target directory, or {@code null} for the JVM default + */ + public static void setDefaultTempDirectory(File directory) { + defaultTempDirectory = directory; + } + + /** + * @return the process-wide default directory, or {@code null} if none is configured + */ + public static File getDefaultTempDirectory() { + return defaultTempDirectory; + } + + /** + * Sets the temporary-file directory for the current thread only. It takes precedence over the + * process-wide default and should be cleared when the thread is done ({@link #clearTempDirectory()}). + * + * @param directory target directory for this thread, or {@code null} to fall back to the default + */ + public static void setTempDirectory(File directory) { + if (directory == null) { + TEMP_DIRECTORY.remove(); + } else { + TEMP_DIRECTORY.set(directory); + } + } + + /** + * Removes the per-thread temporary-file directory, falling back to the process-wide default. + */ + public static void clearTempDirectory() { + TEMP_DIRECTORY.remove(); + } + + /** + * @return the directory that will be used for new temporary files on the current thread: the + * per-thread override if set, otherwise the process-wide default, otherwise {@code null} (JVM default) + */ + public static File getTempDirectory() { + File perThread = TEMP_DIRECTORY.get(); + return perThread != null ? perThread : defaultTempDirectory; + } + + /** + * Creates a temporary file, honouring the configured directory. Equivalent to + * {@link File#createTempFile(String, String)} when no directory is configured. + * + * @param prefix file-name prefix, as for {@link File#createTempFile(String, String, File)} + * @param suffix file-name suffix, or {@code null} + * @return the newly created temporary file + * @throws IOException if the file could not be created + */ + public static File createTempFile(String prefix, String suffix) throws IOException { + File directory = getTempDirectory(); + return directory != null + ? File.createTempFile(prefix, suffix, directory) + : File.createTempFile(prefix, suffix); + } +}