From 89b39f65dbba0e940e609c21a04b8c8a9ff174be Mon Sep 17 00:00:00 2001 From: Christian Voss Date: Mon, 21 Sep 2026 08:39:35 +0200 Subject: [PATCH 1/3] Unmap memory-mapped files deterministically on Java 22+ (FFM, multi-release jar) Fixes #1112, #1517 --- openpdf-core/pom.xml | 63 +++++ .../text/pdf/MappedRandomAccessFile.java | 35 ++- .../text/utils/LongMappedByteBuffer.java | 21 +- .../text/utils/LongMappedByteBuffer.java | 221 ++++++++++++++++++ .../openpdf/text/pdf/MappedFileReleaseIT.java | 117 ++++++++++ .../text/utils/LongMappedByteBufferTest.java | 143 ++++++++++++ 6 files changed, 590 insertions(+), 10 deletions(-) create mode 100644 openpdf-core/src/main/java22/org/openpdf/text/utils/LongMappedByteBuffer.java create mode 100644 openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseIT.java create mode 100644 openpdf-core/src/test/java/org/openpdf/text/utils/LongMappedByteBufferTest.java diff --git a/openpdf-core/pom.xml b/openpdf-core/pom.xml index 247e66c88..6cc16ff70 100644 --- a/openpdf-core/pom.xml +++ b/openpdf-core/pom.xml @@ -101,4 +101,67 @@ + + + + + multi-release-java22 + + [22,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile-java22 + + compile + + + 22 + + ${project.basedir}/src/main/java22 + + true + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + multi-release-java22-tests + + integration-test + verify + + + ${project.build.directory}/${project.build.finalName}.jar + + **/LongMappedByteBufferTest.java + **/MappedFileReleaseIT.java + + + + + + + + + diff --git a/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java b/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java index fb0db37fb..3018bc9d2 100644 --- a/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java +++ b/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java @@ -97,11 +97,19 @@ public MappedRandomAccessFile(String filename, String mode) private void init(FileChannel channel, FileChannel.MapMode mapMode) throws IOException { - this.channel = channel; - this.mappedByteBuffer = new LongMappedByteBuffer(channel, mapMode); - - mappedByteBuffer.load(); + try { + this.mappedByteBuffer = new LongMappedByteBuffer(channel, mapMode); + mappedByteBuffer.load(); + } catch (IOException | RuntimeException | Error e) { + // don't leak the file handle (and the mapping) if the file cannot be mapped + try { + close(); + } catch (IOException suppressed) { + e.addSuppressed(suppressed); + } + throw e; + } } /** @@ -194,17 +202,26 @@ public long length() { } /** - * Cleans the mapped bytebuffer and closes the channel + * Cleans the mapped bytebuffer and closes the channel. + *

+ * On Java 22 and later, the mapping is released immediately, so the file can be deleted or moved right after this + * call, also on Windows. On Java 21, the mapping is released when the buffer is garbage collected. * * @throws IOException on error * @see java.io.RandomAccessFile#close() */ public void close() throws IOException { - mappedByteBuffer = null; - if (channel != null) { - channel.close(); + try { + if (mappedByteBuffer != null) { + mappedByteBuffer.close(); + } + } finally { + mappedByteBuffer = null; + if (channel != null) { + channel.close(); + } + channel = null; } - channel = null; } } diff --git a/openpdf-core/src/main/java/org/openpdf/text/utils/LongMappedByteBuffer.java b/openpdf-core/src/main/java/org/openpdf/text/utils/LongMappedByteBuffer.java index b41adf437..6b17663db 100644 --- a/openpdf-core/src/main/java/org/openpdf/text/utils/LongMappedByteBuffer.java +++ b/openpdf-core/src/main/java/org/openpdf/text/utils/LongMappedByteBuffer.java @@ -54,15 +54,20 @@ import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; +import java.util.Arrays; /** * A utility class that allows random access to files larger than 2GB by internally * mapping them into multiple {@link MappedByteBuffer} chunks of up to 2GB each. + *

+ * This is the Java 21 implementation. On Java 22 and later, the multi-release variant of this class (compiled from + * {@code src/main/java22}) is used instead. It maps the file with the Foreign Function & Memory API, so that + * {@link #close()} releases the mapping immediately. * * @since 2.0.4 */ -public class LongMappedByteBuffer { +public class LongMappedByteBuffer implements AutoCloseable { private static final long CHUNK_SIZE = Integer.MAX_VALUE; // 2 GB private final MappedByteBuffer[] chunks; @@ -233,4 +238,18 @@ public void force() { chunk.force(); } } + + /** + * Releases the references to the mapped chunks. The buffer must not be used after it has been closed. + *

+ * Java 21 offers no supported way to unmap a {@link MappedByteBuffer} explicitly, so the mapping itself (and with + * it the lock on the file on Windows) is only released once the chunks have been garbage collected. On Java 22 + * and later, the multi-release variant of this class releases the mapping immediately. + * + * @since 3.0.6 + */ + @Override + public void close() { + Arrays.fill(chunks, null); + } } diff --git a/openpdf-core/src/main/java22/org/openpdf/text/utils/LongMappedByteBuffer.java b/openpdf-core/src/main/java22/org/openpdf/text/utils/LongMappedByteBuffer.java new file mode 100644 index 000000000..ee0474bd9 --- /dev/null +++ b/openpdf-core/src/main/java22/org/openpdf/text/utils/LongMappedByteBuffer.java @@ -0,0 +1,221 @@ +/* + * OpenPDF, LongMappedByteBuffer. + * + * Copyright 2025 Andreas Røsdal + * + * The contents of this file are subject to the Mozilla Public License Version 1.1 + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the License. + * + * The Original Code is 'iText, a free JAVA-PDF library'. + * + * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by + * the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie. + * All Rights Reserved. + * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer + * are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved. + * + * Contributor(s): all the names of the contributors are added in the source code + * where applicable. + * + * Alternatively, the contents of this file may be used under the terms of the + * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the + * provisions of LGPL are applicable instead of those above. If you wish to + * allow use of your version of this file only under the terms of the LGPL + * License and not to allow others to use your version of this file under + * the MPL, indicate your decision by deleting the provisions above and + * replace them with the notice and other provisions required by the LGPL. + * If you do not delete the provisions above, a recipient may use your version + * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MPL as stated above or under the terms of the GNU + * Library General Public License as published by the Free Software Foundation; + * either version 2 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more + * details. + * + * If you didn't download this code from the following link, you should check if + * you aren't using an obsolete version: + * https://github.com/LibrePDF/OpenPDF + */ + +package org.openpdf.text.utils; + +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.BufferUnderflowException; +import java.nio.ReadOnlyBufferException; +import java.nio.channels.FileChannel; + + +/** + * A utility class that allows random access to memory-mapped files, including files larger than 2GB. + *

+ * This is the Java 22+ variant of this class, packaged in {@code META-INF/versions/22} of the multi-release jar. It + * has the same public API and behavior as the Java 21 implementation, but maps the file with the Foreign Function + * & Memory API (JEP 454) into a single {@link MemorySegment} that belongs to its own {@link Arena}. Closing this + * buffer closes the arena, which unmaps the file immediately instead of waiting for the garbage collector. This + * releases the lock that Windows holds on mapped files, so they can be deleted or moved right after + * {@link #close()}. + *

+ * A shared arena is used, because a {@code PdfReader} may be created in one thread and read in another. Accessing + * the buffer after it has been closed throws an {@link IllegalStateException}; it never accesses unmapped memory. + * + * @since 2.0.4 + */ +public class LongMappedByteBuffer implements AutoCloseable { + + private final Arena arena; + private final MemorySegment segment; + private final long size; + + private long position = 0; + + /** + * Constructs a new LongMappedByteBuffer by mapping the file channel. + */ + public LongMappedByteBuffer(FileChannel channel, FileChannel.MapMode mode) throws IOException { + this.size = channel.size(); + Arena mappingArena = Arena.ofShared(); + try { + this.segment = channel.map(mode, 0, size, mappingArena); + } catch (Throwable t) { + mappingArena.close(); + throw t; + } + this.arena = mappingArena; + } + + public byte get() { + byte b = get(position); + position++; + return b; + } + + public byte get(long pos) { + if (pos >= size) { + throw new BufferUnderflowException(); // triggers EOF handling in MappedRandomAccessFile + } + return segment.get(ValueLayout.JAVA_BYTE, pos); + } + + public void get(long pos, byte[] dst, int off, int len) { + if (off < 0 || len < 0 || off + len > dst.length) { + throw new IndexOutOfBoundsException("Invalid offset/length"); + } + if (len == 0) { + return; + } + MemorySegment.copy(segment, ValueLayout.JAVA_BYTE, pos, dst, off, len); + } + + public void get(byte[] dst, int off, int len) { + get(position, dst, off, len); + position += len; + } + + public void put(byte value) { + put(position, value); + position++; + } + + public void put(long pos, byte value) { + checkWritable(); + segment.set(ValueLayout.JAVA_BYTE, pos, value); + } + + public void put(byte[] src, int off, int len) { + if (off < 0 || len < 0 || off + len > src.length) { + throw new IndexOutOfBoundsException("Invalid offset/length"); + } + if (len == 0) { + return; + } + checkWritable(); + MemorySegment.copy(src, off, segment, ValueLayout.JAVA_BYTE, position, len); + position += len; + } + + public int read(byte[] bytes, int off, int len) { + long pos = position(); + long limit = limit(); + + if (pos >= limit) { + return -1; + } + + int available = (int) Math.min(len, limit - pos); + + get(pos, bytes, off, available); // will throw if something is wrong + position(pos + available); + + return available; + } + + public long position() { + return position; + } + + public LongMappedByteBuffer position(long newPosition) { + if (newPosition < 0 || newPosition > size) { + throw new IllegalArgumentException("Position out of bounds"); + } + + this.position = newPosition; + return this; + } + + public long size() { + return size; + } + + public long limit() { + return size; + } + + public LongMappedByteBuffer load() { + segment.load(); + return this; + } + + public boolean isLoaded() { + return segment.isLoaded(); + } + + public void force() { + segment.force(); + } + + /** + * Unmaps the file immediately. The buffer must not be used after it has been closed. Calling this method more + * than once has no effect. + * + * @since 3.0.6 + */ + @Override + public void close() { + // not synchronized in the signature: the public API must be identical to the Java 21 class (jar --validate) + synchronized (this) { + if (arena.scope().isAlive()) { + arena.close(); + } + } + } + + private void checkWritable() { + // same exception as the MappedByteBuffer based implementation, instead of an IllegalArgumentException + if (segment.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + } +} diff --git a/openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseIT.java b/openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseIT.java new file mode 100644 index 000000000..92bedfa9a --- /dev/null +++ b/openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseIT.java @@ -0,0 +1,117 @@ +package org.openpdf.text.pdf; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.openpdf.text.Document; +import org.openpdf.text.Paragraph; +import org.openpdf.text.utils.LongMappedByteBuffer; + +/** + * On Java 22+, memory-mapped files must be unmapped as soon as they are closed, without waiting for the garbage + * collector (issues #1112, #1517). Runs with failsafe against the multi-release jar. + *

+ * On Windows, a mapped file cannot be deleted, so deleting the file verifies the release there. On Linux, a mapped + * file can always be deleted, so the test checks /proc/self/maps instead. + *

+ * There is intentionally no {@code System.gc()} in this test. + */ +class MappedFileReleaseIT { + + @TempDir + Path tempDir; + + @BeforeEach + void requireJava22() { + assumeTrue(Runtime.version().feature() >= 22, "deterministic unmapping requires Java 22+"); + } + + @Test + void bufferCannotBeAccessedAfterClose() throws IOException { + Path file = Files.write(tempDir.resolve("data.bin"), new byte[]{1, 2, 3}); + try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) { + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY); + assertEquals(1, buffer.get(0)); + buffer.close(); + // the FFM variant is in use: the mapping is gone instead of just unreferenced + assertThrows(IllegalStateException.class, () -> buffer.get(0)); + } + } + + @Test + void mappedRandomAccessFileReleasesFileOnClose() throws IOException { + Path file = Files.write(tempDir.resolve("data.bin"), new byte[]{1, 2, 3}); + String realPath = file.toRealPath().toString(); + + MappedRandomAccessFile raf = new MappedRandomAccessFile(file.toString(), "r"); + assertEquals(1, raf.read()); + assertMappedIfDetectable(realPath, true); + raf.close(); + + assertMappedIfDetectable(realPath, false); + Files.delete(file); + } + + @Test + void pdfReaderReleasesFileOnClose() throws IOException { + Path pdf = createPdf(); + String realPath = pdf.toRealPath().toString(); + assertFalse(Document.plainRandomAccess, "test requires memory-mapped access"); + + try (PdfReader reader = new PdfReader(pdf.toString())) { + assertEquals(1, reader.getNumberOfPages()); + } + + assertMappedIfDetectable(realPath, false); + Files.delete(pdf); + } + + @Test + void partialPdfReaderReleasesFileOnClose() throws IOException { + Path pdf = createPdf(); + String realPath = pdf.toRealPath().toString(); + + try (PdfReader reader = new PdfReader(new RandomAccessFileOrArray(pdf.toString()), null)) { + assertEquals(1, reader.getNumberOfPages()); + assertMappedIfDetectable(realPath, true); + } + + assertMappedIfDetectable(realPath, false); + Files.delete(pdf); + } + + private Path createPdf() throws IOException { + Path pdf = tempDir.resolve("document.pdf"); + try (OutputStream out = Files.newOutputStream(pdf)) { + Document document = new Document(); + PdfWriter.getInstance(document, out); + document.open(); + document.add(new Paragraph("Hello")); + document.close(); + } + return pdf; + } + + private static void assertMappedIfDetectable(String realPath, boolean expected) throws IOException { + Path maps = Path.of("/proc/self/maps"); + if (!Files.isReadable(maps)) { + return; // not Linux; on Windows, Files.delete() verifies the release + } + try (Stream lines = Files.lines(maps)) { + assertEquals(expected, lines.anyMatch(line -> line.endsWith(realPath)), + () -> realPath + (expected ? " should be mapped" : " should no longer be mapped")); + } + } +} diff --git a/openpdf-core/src/test/java/org/openpdf/text/utils/LongMappedByteBufferTest.java b/openpdf-core/src/test/java/org/openpdf/text/utils/LongMappedByteBufferTest.java new file mode 100644 index 000000000..854e9987b --- /dev/null +++ b/openpdf-core/src/test/java/org/openpdf/text/utils/LongMappedByteBufferTest.java @@ -0,0 +1,143 @@ +package org.openpdf.text.utils; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ReadOnlyBufferException; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Behavior of {@link LongMappedByteBuffer} that the Java 21 implementation and the Java 22+ multi-release variant + * must have in common. Surefire runs this test against the Java 21 classes; on JDK 22+, failsafe runs it again + * against the packaged multi-release jar. + */ +class LongMappedByteBufferTest { + + private static final byte[] CONTENT = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, (byte) 0xff}; + + @TempDir + Path tempDir; + + @Test + void readsWholeFileByteByByte() throws IOException { + try (FileChannel channel = open(write(CONTENT)); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY)) { + assertEquals(CONTENT.length, buffer.size()); + assertEquals(CONTENT.length, buffer.limit()); + for (byte expected : CONTENT) { + assertEquals(expected, buffer.get()); + } + assertEquals(CONTENT.length, buffer.position()); + } + } + + @Test + void getAtEndThrowsBufferUnderflowException() throws IOException { + try (FileChannel channel = open(write(CONTENT)); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY)) { + assertThrows(BufferUnderflowException.class, () -> buffer.get(CONTENT.length)); + } + } + + @Test + void readReturnsAvailableBytesAndMinusOneAtEnd() throws IOException { + try (FileChannel channel = open(write(CONTENT)); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY)) { + buffer.position(CONTENT.length - 3); + byte[] dst = new byte[10]; + assertEquals(3, buffer.read(dst, 2, 10 - 2)); + assertArrayEquals(new byte[]{0, 0, 8, 9, (byte) 0xff, 0, 0, 0, 0, 0}, dst); + assertEquals(CONTENT.length, buffer.position()); + assertEquals(-1, buffer.read(dst, 0, dst.length)); + } + } + + @Test + void bulkGetCopiesRangeAndRejectsInvalidArguments() throws IOException { + try (FileChannel channel = open(write(CONTENT)); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY)) { + byte[] dst = new byte[4]; + buffer.get(3, dst, 0, 4); + assertArrayEquals(new byte[]{3, 4, 5, 6}, dst); + assertDoesNotThrow(() -> buffer.get(CONTENT.length, dst, 0, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> buffer.get(0, dst, 1, 4)); + } + } + + @Test + void positionOutsideOfFileIsRejected() throws IOException { + try (FileChannel channel = open(write(CONTENT)); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY)) { + assertThrows(IllegalArgumentException.class, () -> buffer.position(-1)); + assertThrows(IllegalArgumentException.class, () -> buffer.position(CONTENT.length + 1)); + assertDoesNotThrow(() -> buffer.position(CONTENT.length)); + } + } + + @Test + void emptyFileCanBeMapped() throws IOException { + try (FileChannel channel = open(write(new byte[0])); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY)) { + assertEquals(0, buffer.size()); + buffer.load(); + assertEquals(-1, buffer.read(new byte[1], 0, 1)); + } + } + + @Test + void writingToReadOnlyMappingThrowsReadOnlyBufferException() throws IOException { + try (FileChannel channel = open(write(CONTENT)); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY)) { + assertThrows(ReadOnlyBufferException.class, () -> buffer.put((byte) 1)); + assertThrows(ReadOnlyBufferException.class, () -> buffer.put(0, (byte) 1)); + assertThrows(ReadOnlyBufferException.class, () -> buffer.put(new byte[]{1}, 0, 1)); + assertDoesNotThrow(buffer::force); + } + } + + @Test + void writesToReadWriteMappingReachTheFile() throws IOException { + Path file = write(CONTENT); + try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE); + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_WRITE)) { + buffer.put(0, (byte) 42); + buffer.position(1); + buffer.put(new byte[]{43, 44}, 0, 2); + buffer.put((byte) 45); + assertEquals(4, buffer.position()); + buffer.force(); + } + byte[] expected = CONTENT.clone(); + expected[0] = 42; + expected[1] = 43; + expected[2] = 44; + expected[3] = 45; + assertArrayEquals(expected, Files.readAllBytes(file)); + } + + @Test + void closeCanBeCalledMoreThanOnce() throws IOException { + try (FileChannel channel = open(write(CONTENT))) { + LongMappedByteBuffer buffer = new LongMappedByteBuffer(channel, FileChannel.MapMode.READ_ONLY); + buffer.close(); + assertDoesNotThrow(buffer::close); + } + } + + private Path write(byte[] content) throws IOException { + return Files.write(Files.createTempFile(tempDir, "mapped", ".bin"), content); + } + + private static FileChannel open(Path file) throws IOException { + return FileChannel.open(file, StandardOpenOption.READ); + } +} From 364e69c894f36893f7e03d9542f99c8d915d697b Mon Sep 17 00:00:00 2001 From: Christian Voss Date: Mon, 21 Sep 2026 11:17:45 +0200 Subject: [PATCH 2/3] rename MappedFileReleaseIT to MappedFileReleaseTest to fit the checkstyle rules --- openpdf-core/pom.xml | 16 +++++++++++++++- ...ReleaseIT.java => MappedFileReleaseTest.java} | 7 +++++-- 2 files changed, 20 insertions(+), 3 deletions(-) rename openpdf-core/src/test/java/org/openpdf/text/pdf/{MappedFileReleaseIT.java => MappedFileReleaseTest.java} (91%) diff --git a/openpdf-core/pom.xml b/openpdf-core/pom.xml index 6cc16ff70..c60bb7f95 100644 --- a/openpdf-core/pom.xml +++ b/openpdf-core/pom.xml @@ -99,6 +99,20 @@ true + + org.apache.maven.plugins + maven-surefire-plugin + + + + **/MappedFileReleaseTest.java + + + @@ -154,7 +168,7 @@ ${project.build.directory}/${project.build.finalName}.jar **/LongMappedByteBufferTest.java - **/MappedFileReleaseIT.java + **/MappedFileReleaseTest.java diff --git a/openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseIT.java b/openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseTest.java similarity index 91% rename from openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseIT.java rename to openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseTest.java index 92bedfa9a..3b3b0efd0 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseIT.java +++ b/openpdf-core/src/test/java/org/openpdf/text/pdf/MappedFileReleaseTest.java @@ -21,14 +21,17 @@ /** * On Java 22+, memory-mapped files must be unmapped as soon as they are closed, without waiting for the garbage - * collector (issues #1112, #1517). Runs with failsafe against the multi-release jar. + * collector (issues #1112, #1517). This is an integration test: it is excluded from the default surefire run (which + * uses {@code target/classes}, where the JVM ignores {@code META-INF/versions}) and instead runs with failsafe + * against the packaged multi-release jar, so it actually exercises the Java 22+ variant of + * {@link LongMappedByteBuffer}. *

* On Windows, a mapped file cannot be deleted, so deleting the file verifies the release there. On Linux, a mapped * file can always be deleted, so the test checks /proc/self/maps instead. *

* There is intentionally no {@code System.gc()} in this test. */ -class MappedFileReleaseIT { +class MappedFileReleaseTest { @TempDir Path tempDir; From f1a06ce9f011bfc553f142fd7b9cd9d78866a301 Mon Sep 17 00:00:00 2001 From: Christian Voss Date: Wed, 23 Sep 2026 08:02:31 +0200 Subject: [PATCH 3/3] fix SonarQube hint --- .../main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java b/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java index 3018bc9d2..0bed07e39 100644 --- a/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java +++ b/openpdf-core/src/main/java/org/openpdf/text/pdf/MappedRandomAccessFile.java @@ -101,7 +101,7 @@ private void init(FileChannel channel, FileChannel.MapMode mapMode) try { this.mappedByteBuffer = new LongMappedByteBuffer(channel, mapMode); mappedByteBuffer.load(); - } catch (IOException | RuntimeException | Error e) { + } catch (IOException | RuntimeException e) { // don't leak the file handle (and the mapping) if the file cannot be mapped try { close();