From 44495f931ddd852d0e273d431c10aa0c102d576c Mon Sep 17 00:00:00 2001 From: yawkat Date: Sat, 12 Sep 2026 02:07:10 +0000 Subject: [PATCH 1/3] Make default decompression limit configurable Co-Authored-By: multicode --- .../lz4/LZ4DecompressorWithLength.java | 36 ++++++++++++++----- src/test/net/jpountz/lz4/OutOfBoundsTest.java | 15 ++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java index ee38ee1..450c2bc 100644 --- a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java +++ b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java @@ -28,13 +28,21 @@ * The user does not need to specify the length of the compressed data or * original data because the length of the original decompressed data is * included in the compressed data. + * For large or unknown-size inputs, prefer {@link LZ4FrameInputStream} to avoid + * buffering the complete decompressed data in memory. + * The default maximum decompressed length for methods that allocate their + * output buffer can be overridden at class initialization with the + * {@code net.jpountz.lz4.LZ4DecompressorWithLength.maxDecompressedLength} + * system property, specified as a number of bytes. Constructors with an + * explicit maximum are not affected by this property. */ public class LZ4DecompressorWithLength { // Each LZ4 match-length extension byte adds at most 255 decompressed bytes. private static final int MAX_COMPRESSION_RATIO = 255; - private static final int DEFAULT_MAX_DECOMPRESSED_LENGTH = 64 * 1024 * 1024; + private static final int DEFAULT_MAX_DECOMPRESSED_LENGTH = Integer.getInteger( + "net.jpountz.lz4.LZ4DecompressorWithLength.maxDecompressedLength", 64 * 1024 * 1024); private final LZ4FastDecompressor fastDecompressor; private final LZ4SafeDecompressor safeDecompressor; @@ -137,22 +145,34 @@ public LZ4DecompressorWithLength(LZ4SafeDecompressor safeDecompressor, int maxDe } private void checkDecompressedLength(int decompressedLength) { - if (decompressedLength < 0 || decompressedLength > maxDecompressedLength) { - throw new LZ4Exception("Invalid decompressed length"); + if (decompressedLength < 0) { + throw new LZ4Exception("Invalid decompressed length: " + decompressedLength); + } + if (decompressedLength > maxDecompressedLength) { + throw new LZ4Exception("Decompressed length " + decompressedLength + + " exceeds configured maximum " + maxDecompressedLength); } } private void checkDecompressedLength(int decompressedLength, int compressedLength) { - if (compressedLength < 0 - || decompressedLength > (long) compressedLength * MAX_COMPRESSION_RATIO) { - throw new LZ4Exception("Invalid decompressed length"); + if (compressedLength < 0) { + throw new LZ4Exception("Invalid compressed length: " + compressedLength); + } + if (decompressedLength > (long) compressedLength * MAX_COMPRESSION_RATIO) { + throw new LZ4Exception("Decompressed length " + decompressedLength + + " exceeds maximum compression ratio of " + MAX_COMPRESSION_RATIO + + " for compressed length " + compressedLength); } checkDecompressedLength(decompressedLength); } private static void checkDestinationLength(int decompressedLength, int maxDestinationLength) { - if (decompressedLength < 0 || decompressedLength > maxDestinationLength) { - throw new LZ4Exception("Invalid decompressed length"); + if (decompressedLength < 0) { + throw new LZ4Exception("Invalid decompressed length: " + decompressedLength); + } + if (decompressedLength > maxDestinationLength) { + throw new LZ4Exception("Decompressed length " + decompressedLength + + " exceeds destination length " + maxDestinationLength); } } diff --git a/src/test/net/jpountz/lz4/OutOfBoundsTest.java b/src/test/net/jpountz/lz4/OutOfBoundsTest.java index daceeb9..3416190 100644 --- a/src/test/net/jpountz/lz4/OutOfBoundsTest.java +++ b/src/test/net/jpountz/lz4/OutOfBoundsTest.java @@ -130,11 +130,12 @@ public void impossibleDeclaredLength(LZ4DecompressorWithLength decompressor) { 0 // empty LZ4 block }; LZ4Exception exception = assertThrows(LZ4Exception.class, () -> decompressor.decompress(input)); - assertEquals("Invalid decompressed length", exception.getMessage()); + assertEquals("Decompressed length 1048576 exceeds maximum compression ratio of 255 for compressed length 1", + exception.getMessage()); Arrays.fill(input, 0, 4, (byte) 0xff); // negative decompressed length exception = assertThrows(LZ4Exception.class, () -> decompressor.decompress(input)); - assertEquals("Invalid decompressed length", exception.getMessage()); + assertEquals("Invalid decompressed length: -1", exception.getMessage()); int declaredLength = 64 * 1024 * 1024 + 1; byte[] defaultLimitInput = new byte[4 + (declaredLength + 254) / 255]; @@ -143,7 +144,7 @@ public void impossibleDeclaredLength(LZ4DecompressorWithLength decompressor) { defaultLimitInput[2] = (byte) (declaredLength >>> 16); defaultLimitInput[3] = (byte) (declaredLength >>> 24); exception = assertThrows(LZ4Exception.class, () -> decompressor.decompress(defaultLimitInput)); - assertEquals("Invalid decompressed length", exception.getMessage()); + assertEquals("Decompressed length 67108865 exceeds configured maximum 67108864", exception.getMessage()); } @ParameterizedTest @@ -155,13 +156,13 @@ public void destinationBufferLimitsDecompressedLength(LZ4DecompressorWithLength byte[] destination = new byte[16]; LZ4Exception exception = assertThrows(LZ4Exception.class, () -> decompressor.decompress(compressed, 0, destination, 1)); - assertEquals("Invalid decompressed length", exception.getMessage()); + assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); ByteBuffer movingSrc = ByteBuffer.wrap(compressed); ByteBuffer movingDest = ByteBuffer.allocate(15); exception = assertThrows(LZ4Exception.class, () -> decompressor.decompress(movingSrc, movingDest)); - assertEquals("Invalid decompressed length", exception.getMessage()); + assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); assertEquals(0, movingSrc.position()); assertEquals(0, movingDest.position()); @@ -169,7 +170,7 @@ public void destinationBufferLimitsDecompressedLength(LZ4DecompressorWithLength ByteBuffer indexedDest = ByteBuffer.allocate(16); exception = assertThrows(LZ4Exception.class, () -> decompressor.decompress(indexedSrc, 0, indexedDest, 1)); - assertEquals("Invalid decompressed length", exception.getMessage()); + assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); assertEquals(0, indexedSrc.position()); assertEquals(0, indexedDest.position()); } @@ -181,7 +182,7 @@ public void configuredMaximumOnlyLimitsAllocatedOutput(LZ4DecompressorWithLength byte[] compressed = new LZ4CompressorWithLength(factory.fastCompressor()).compress(new byte[16]); LZ4Exception exception = assertThrows(LZ4Exception.class, () -> decompressor.decompress(compressed)); - assertEquals("Invalid decompressed length", exception.getMessage()); + assertEquals("Decompressed length 16 exceeds configured maximum 15", exception.getMessage()); byte[] destination = new byte[16]; decompressor.decompress(compressed, destination); From 73abbad9e994adfe1619a302d3039ca59a2191e7 Mon Sep 17 00:00:00 2001 From: yawkat Date: Sat, 12 Sep 2026 02:07:10 +0000 Subject: [PATCH 2/3] Document streaming for length-prefixed compression Co-Authored-By: multicode --- src/java/net/jpountz/lz4/LZ4CompressorWithLength.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/java/net/jpountz/lz4/LZ4CompressorWithLength.java b/src/java/net/jpountz/lz4/LZ4CompressorWithLength.java index 3a2d566..fcd4e77 100644 --- a/src/java/net/jpountz/lz4/LZ4CompressorWithLength.java +++ b/src/java/net/jpountz/lz4/LZ4CompressorWithLength.java @@ -26,6 +26,12 @@ * {@link LZ4DecompressorWithLength} and is NOT compatible with any other * decompressors in lz4-java or any other lz4 tools. This class deliberately * does not extend {@link LZ4Compressor} because they are not interchangable. + * Methods in {@link LZ4DecompressorWithLength} that allocate their output + * buffer reject decompressed lengths greater than 64 MiB by default. For large + * or unknown-size inputs, prefer {@link LZ4FrameOutputStream} to avoid buffering + * the complete input in memory. The default can be overridden with the + * {@code net.jpountz.lz4.LZ4DecompressorWithLength.maxDecompressedLength} + * system property. */ public class LZ4CompressorWithLength { From 6626eb323e5416212cbdc8b5ca2fe99039ee03fc Mon Sep 17 00:00:00 2001 From: yawkat Date: Sat, 12 Sep 2026 02:13:10 +0000 Subject: [PATCH 3/3] Clarify configurable constructor limits Co-Authored-By: multicode --- src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java index 450c2bc..3f5484e 100644 --- a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java +++ b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java @@ -92,7 +92,9 @@ public static int getDecompressedLength(ByteBuffer src, int srcOff) { /** * Creates a new decompressor to decompress data compressed by {@link LZ4CompressorWithLength}. - * Methods that allocate their output buffer reject decompressed lengths greater than 64 MiB. + * Methods that allocate their output buffer reject decompressed lengths greater than 64 MiB by default. + * This fallback can be overridden with the + * {@code net.jpountz.lz4.LZ4DecompressorWithLength.maxDecompressedLength} system property. * Note that it is deprecated to use a JNI-binding instance of {@link LZ4FastDecompressor}. * Please see {@link LZ4Factory#nativeInstance()} for details. * @@ -121,7 +123,9 @@ public LZ4DecompressorWithLength(LZ4FastDecompressor fastDecompressor, int maxDe /** * Creates a new decompressor to decompress data compressed by {@link LZ4CompressorWithLength}. - * Methods that allocate their output buffer reject decompressed lengths greater than 64 MiB. + * Methods that allocate their output buffer reject decompressed lengths greater than 64 MiB by default. + * This fallback can be overridden with the + * {@code net.jpountz.lz4.LZ4DecompressorWithLength.maxDecompressedLength} system property. * * @param safeDecompressor safe decompressor to use */