From 045b9c88cb49a90981661fff17c080d51af4dcd9 Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Fri, 3 Apr 2026 13:19:38 +0300 Subject: [PATCH 1/9] Add native fast reset compressors --- src/java/net/jpountz/lz4/LZ4Constants.java | 3 + src/java/net/jpountz/lz4/LZ4Factory.java | 124 +++++++++ src/java/net/jpountz/lz4/LZ4JNI.java | 84 +++++++ .../lz4/LZ4JNIFastResetCompressor.java | 221 ++++++++++++++++ .../lz4/LZ4JNIHCFastResetCompressor.java | 215 ++++++++++++++++ src/jni/net_jpountz_lz4_LZ4JNI.c | 235 +++++++++++++++++- 6 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java create mode 100644 src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java diff --git a/src/java/net/jpountz/lz4/LZ4Constants.java b/src/java/net/jpountz/lz4/LZ4Constants.java index 3f33b37c..6bab187d 100644 --- a/src/java/net/jpountz/lz4/LZ4Constants.java +++ b/src/java/net/jpountz/lz4/LZ4Constants.java @@ -22,6 +22,9 @@ enum LZ4Constants { static final int DEFAULT_COMPRESSION_LEVEL = 8 + 1; static final int MAX_COMPRESSION_LEVEL = 16 + 1; + static final int DEFAULT_ACCELERATION = 1; + static final int MIN_ACCELERATION = 1; + static final int MAX_ACCELERATION = 65537; static final int MEMORY_USAGE = 14; static final int NOT_COMPRESSIBLE_DETECTION_LEVEL = 6; diff --git a/src/java/net/jpountz/lz4/LZ4Factory.java b/src/java/net/jpountz/lz4/LZ4Factory.java index 6110b2cc..00d74b5c 100644 --- a/src/java/net/jpountz/lz4/LZ4Factory.java +++ b/src/java/net/jpountz/lz4/LZ4Factory.java @@ -26,6 +26,8 @@ import static net.jpountz.lz4.LZ4Constants.DEFAULT_COMPRESSION_LEVEL; import static net.jpountz.lz4.LZ4Constants.MAX_COMPRESSION_LEVEL; +import static net.jpountz.lz4.LZ4Constants.MIN_ACCELERATION; +import static net.jpountz.lz4.LZ4Constants.MAX_ACCELERATION; /** * Entry point for the LZ4 API. @@ -319,6 +321,122 @@ public LZ4Compressor highCompressor(int compressionLevel) { return highCompressors[compressionLevel]; } + /** + * Creates a new {@link LZ4JNIFastResetCompressor} with default acceleration (1). + *

+ * This compressor pre-allocates native state once and uses + * {@code LZ4_compress_fast_extState_fastReset} for each compression, avoiding + * the expensive full state initialization that the default {@link #fastCompressor()} + * performs on every call. + *

+ * Unlike {@link #fastCompressor()}, this compressor is NOT thread-safe and + * holds native resources that must be freed. Always use try-with-resources or + * call {@link LZ4JNIFastResetCompressor#close() close()} explicitly. + *

+ * Only available for the native instance. + * + * @return a new fast-reset compressor + * @throws UnsupportedOperationException if this is not the native instance + * @see LZ4JNIFastResetCompressor + * @see #fastCompressor() + * @see #fastResetCompressor(int) + */ + public LZ4JNIFastResetCompressor fastResetCompressor() { + enforceNativeInstance("fastResetCompressor"); + return new LZ4JNIFastResetCompressor(); + } + + /** + * Creates a new {@link LZ4JNIFastResetCompressor} with the specified acceleration. + *

+ * This compressor pre-allocates native state once and uses + * {@code LZ4_compress_fast_extState_fastReset} for each compression, avoiding + * the expensive full state initialization that the default {@link #fastCompressor()} + * performs on every call. + *

+ * Unlike {@link #fastCompressor()}, this compressor is NOT thread-safe and + * holds native resources that must be freed. Always use try-with-resources or + * call {@link LZ4JNIFastResetCompressor#close() close()} explicitly. + *

+ * Only available for the native instance. + * + *

Acceleration follows the upstream LZ4 contract:

    + *
  1. It should be in range [1, 65537].
  2. + *
  3. A value lower than 1 is treated as 1.
  4. + *
  5. A value greater than 65537 is treated as 65537.
  6. + *
+ * + * @param acceleration acceleration factor (1 = default, higher = faster but less compression) + * @return a new fast-reset compressor + * @throws UnsupportedOperationException if this is not the native instance + * @see LZ4JNIFastResetCompressor + * @see #fastCompressor() + */ + public LZ4JNIFastResetCompressor fastResetCompressor(int acceleration) { + enforceNativeInstance("fastResetCompressor"); + if (acceleration < MIN_ACCELERATION) { + acceleration = MIN_ACCELERATION; + } else if (acceleration > MAX_ACCELERATION) { + acceleration = MAX_ACCELERATION; + } + return new LZ4JNIFastResetCompressor(acceleration); + } + + /** + * Creates a new {@link LZ4JNIHCFastResetCompressor} with default compression level (9). + *

+ * This compressor pre-allocates native HC state once and uses + * {@code LZ4_compress_HC_extStateHC_fastReset} for each compression, avoiding + * the expensive full state initialization that the default {@link #highCompressor()} + * performs on every call. + *

+ * Unlike {@link #highCompressor()}, this compressor is NOT thread-safe and + * holds native resources that must be freed. Always use try-with-resources or + * call {@link LZ4JNIHCFastResetCompressor#close() close()} explicitly. + *

+ * Only available for the native instance. + * + * @return a new HC fast-reset compressor + * @throws UnsupportedOperationException if this is not the native instance + * @see LZ4JNIHCFastResetCompressor + * @see #highCompressor() + * @see #highFastResetCompressor(int) + */ + public LZ4JNIHCFastResetCompressor highFastResetCompressor() { + enforceNativeInstance("highFastResetCompressor"); + return new LZ4JNIHCFastResetCompressor(); + } + + /** + * Creates a new {@link LZ4JNIHCFastResetCompressor} with the specified compression level. + *

+ * This compressor pre-allocates native HC state once and uses + * {@code LZ4_compress_HC_extStateHC_fastReset} for each compression, avoiding + * the expensive full state initialization that the default {@link #highCompressor()} + * performs on every call. + *

+ * Unlike {@link #highCompressor()}, this compressor is NOT thread-safe and + * holds native resources that must be freed. Always use try-with-resources or + * call {@link LZ4JNIHCFastResetCompressor#close() close()} explicitly. + *

+ * Only available for the native instance. + * + * @param compressionLevel compression level (1-17, higher = better compression) + * @return a new HC fast-reset compressor + * @throws UnsupportedOperationException if this is not the native instance + * @see LZ4JNIHCFastResetCompressor + * @see #highCompressor(int) + */ + public LZ4JNIHCFastResetCompressor highFastResetCompressor(int compressionLevel) { + enforceNativeInstance("highFastResetCompressor"); + if (compressionLevel > MAX_COMPRESSION_LEVEL) { + compressionLevel = MAX_COMPRESSION_LEVEL; + } else if (compressionLevel < 1) { + compressionLevel = DEFAULT_COMPRESSION_LEVEL; + } + return new LZ4JNIHCFastResetCompressor(compressionLevel); + } + /** * Returns a {@link LZ4FastDecompressor} instance. * Use of this method is deprecated for the {@link #nativeInstance() native instance}. @@ -375,4 +493,10 @@ public String toString() { return getClass().getSimpleName() + ":" + impl; } + private void enforceNativeInstance(String methodName) throws UnsupportedOperationException { + if (!"JNI".equals(impl)) { + throw new UnsupportedOperationException( + methodName + "() is only available for the native instance. Use LZ4Factory.nativeInstance() to get a native factory"); + } + } } diff --git a/src/java/net/jpountz/lz4/LZ4JNI.java b/src/java/net/jpountz/lz4/LZ4JNI.java index 0b5c1617..dfb99283 100644 --- a/src/java/net/jpountz/lz4/LZ4JNI.java +++ b/src/java/net/jpountz/lz4/LZ4JNI.java @@ -39,5 +39,89 @@ enum LZ4JNI { static native int LZ4_decompress_safe(byte[] srcArray, ByteBuffer srcBuffer, int srcOff, int srcLen, byte[] destArray, ByteBuffer destBuffer, int destOff, int maxDestLen); static native int LZ4_compressBound(int len); + /** + * Creates a new LZ4 stream object. + *

+ * The allocated native memory must be freed with {@link #LZ4_freeStream(long)} when no longer needed. + * + * @return pointer to the allocated LZ4_stream_t or 0 on failure + */ + static native long LZ4_createStream(); + + /** + * Frees the native memory allocated for the LZ4 stream object. + * + * @param streamPtr pointer to LZ4_stream_t + * @return 0 on success + */ + static native int LZ4_freeStream(long streamPtr); + + /** + * Compresses using a pre-initialized state with fast-reset semantics. + *

+ * A variant of {@code LZ4_compress_fast_extState()} that avoids an expensive + * full state initialization on every call. + *

+ * The state must have been properly initialized once (e.g. via {@link #LZ4_createStream()}). + * + * @param statePtr pointer to a properly initialized LZ4_stream_t + * @param srcArray source data (byte array), or null if using srcBuffer + * @param srcBuffer source data (direct ByteBuffer), or null if using srcArray + * @param srcOff offset in source + * @param srcLen length to compress + * @param destArray destination buffer (byte array), or null if using destBuffer + * @param destBuffer destination buffer (direct ByteBuffer), or null if using destArray + * @param destOff offset in destination + * @param maxDestLen maximum bytes to write + * @param acceleration acceleration factor (1 = default, higher = faster but less compression) + * @return compressed size, or 0 on failure + */ + static native int LZ4_compress_fast_extState_fastReset(long statePtr, + byte[] srcArray, ByteBuffer srcBuffer, int srcOff, int srcLen, + byte[] destArray, ByteBuffer destBuffer, int destOff, int maxDestLen, + int acceleration); + + /** + * Creates a new LZ4 HC stream object. + *

+ * The allocated native memory must be freed with {@link #LZ4_freeStreamHC(long)} when no longer needed. + * + * @return pointer to the allocated LZ4_streamHC_t or 0 on failure + */ + static native long LZ4_createStreamHC(); + + /** + * Frees the native memory allocated for the LZ4 HC stream object. + * + * @param streamPtr pointer to LZ4_streamHC_t + * @return 0 on success + */ + static native int LZ4_freeStreamHC(long streamPtr); + + /** + * Compresses using a pre-initialized HC state with fast-reset semantics. + *

+ * A variant of {@code LZ4_compress_HC_extStateHC()} that avoids an expensive + * full state initialization on every call. + *

+ * The state must have been properly initialized once (e.g. via {@link #LZ4_createStreamHC()}). + * + * @param statePtr pointer to a properly initialized LZ4_streamHC_t + * @param srcArray source data (byte array), or null if using srcBuffer + * @param srcBuffer source data (direct ByteBuffer), or null if using srcArray + * @param srcOff offset in source + * @param srcLen length to compress + * @param destArray destination buffer (byte array), or null if using destBuffer + * @param destBuffer destination buffer (direct ByteBuffer), or null if using destArray + * @param destOff offset in destination + * @param maxDestLen maximum bytes to write + * @param compressionLevel compression level (1-17, higher = better compression) + * @return compressed size, or 0 on failure + */ + static native int LZ4_compress_HC_extStateHC_fastReset(long statePtr, + byte[] srcArray, ByteBuffer srcBuffer, int srcOff, int srcLen, + byte[] destArray, ByteBuffer destBuffer, int destOff, int maxDestLen, + int compressionLevel); + } diff --git a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java new file mode 100644 index 00000000..bcf992d6 --- /dev/null +++ b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java @@ -0,0 +1,221 @@ +package net.jpountz.lz4; + +/* + * Copyright 2020 Adrien Grand and the lz4-java contributors. + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import net.jpountz.util.ByteBufferUtils; +import net.jpountz.util.SafeUtils; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static net.jpountz.lz4.LZ4Constants.MIN_ACCELERATION; +import static net.jpountz.lz4.LZ4Constants.MAX_ACCELERATION; + +/** + * An optimized LZ4 compressor that uses native {@code LZ4_compress_fast_extState_fastReset}. + *

+ * This compressor pre-allocates an {@code LZ4_stream_t} once and reuses it for every compression + * call through {@code LZ4_compress_fast_extState_fastReset}. This avoids the expensive full + * state initialization that {@link LZ4JNICompressor LZ4_compress_default()} performs on every call. + *

+ * Each compression call is independent, making the output identical to {@link LZ4JNICompressor} + * for the same acceleration level. + *

+ * Thread Safety: This class is NOT thread-safe. Each instance holds + * mutable native state and must be used by only one thread at a time. + *

+ * Resource Management: This class holds native memory that must be freed. + * Always use try-with-resources or explicitly call {@link #close()}. + * + *

Example usage:

+ *
{@code
+ * LZ4Factory factory = LZ4Factory.nativeInstance();
+ * try (LZ4JNIFastResetCompressor compressor = factory.fastResetCompressor()) {
+ *     byte[] compressed = new byte[compressor.maxCompressedLength(data.length)];
+ *     int compressedLen = compressor.compress(data, 0, data.length, compressed, 0, compressed.length);
+ *     // ... use compressed[0..compressedLen-1]
+ * }
+ * }
+ * + * @see LZ4Factory#fastResetCompressor() + * @see LZ4Factory#fastResetCompressor(int) + * @see LZ4JNICompressor + */ +public final class LZ4JNIFastResetCompressor extends LZ4Compressor implements AutoCloseable { + + private final AtomicLong statePtr; + private final int acceleration; + private LZ4Compressor safeInstance; + + /** + * Creates a new fast-reset compressor with default acceleration (1). + * Package-private: use {@link LZ4Factory#fastResetCompressor()}. + */ + LZ4JNIFastResetCompressor() { + this(LZ4Constants.DEFAULT_ACCELERATION); + } + + /** + * Creates a new fast-reset compressor with specified acceleration. + * Package-private: use {@link LZ4Factory#fastResetCompressor(int)}. + * + * @param acceleration acceleration factor (1 = default, higher = faster but less compression) + */ + LZ4JNIFastResetCompressor(int acceleration) { + if (acceleration < MIN_ACCELERATION) { + acceleration = MIN_ACCELERATION; + } else if (acceleration > MAX_ACCELERATION) { + acceleration = MAX_ACCELERATION; + } + this.acceleration = acceleration; + long ptr = LZ4JNI.LZ4_createStream(); + if (ptr == 0) { + throw new LZ4Exception("Failed to allocate LZ4 state"); + } + this.statePtr = new AtomicLong(ptr); + } + + /** + * Returns the acceleration factor. + * + * @return acceleration factor (1 = default) + */ + public int getAcceleration() { + return acceleration; + } + + /** + * Compresses {@code src[srcOff:srcOff+srcLen]} into + * {@code dest[destOff:destOff+maxDestLen]}. + * + * @param src source data + * @param srcOff the start offset in src + * @param srcLen the number of bytes to compress + * @param dest destination buffer + * @param destOff the start offset in dest + * @param maxDestLen the maximum number of bytes to write in dest + * @return the compressed size + * @throws LZ4Exception if maxDestLen is too small + * @throws IllegalStateException if the compressor has been closed + */ + public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { + long ptr = statePtr.get(); + if (ptr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + SafeUtils.checkRange(src, srcOff, srcLen); + SafeUtils.checkRange(dest, destOff, maxDestLen); + + final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( + ptr, src, null, srcOff, srcLen, + dest, null, destOff, maxDestLen, acceleration); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } + + /** + * Compresses {@code src[srcOff:srcOff+srcLen]} into + * {@code dest[destOff:destOff+maxDestLen]}. + *

+ * Both buffers must be either direct or array-backed. If neither, the method + * falls back to the safe Java compressor. + * {@link ByteBuffer} positions remain unchanged. + * + * @param src source data + * @param srcOff the start offset in src + * @param srcLen the number of bytes to compress + * @param dest destination buffer + * @param destOff the start offset in dest + * @param maxDestLen the maximum number of bytes to write in dest + * @return the compressed size + * @throws LZ4Exception if maxDestLen is too small + * @throws IllegalStateException if the compressor has been closed + */ + public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { + long ptr = statePtr.get(); + if (ptr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + ByteBufferUtils.checkNotReadOnly(dest); + ByteBufferUtils.checkRange(src, srcOff, srcLen); + ByteBufferUtils.checkRange(dest, destOff, maxDestLen); + + if ((src.hasArray() || src.isDirect()) && (dest.hasArray() || dest.isDirect())) { + byte[] srcArr = null, destArr = null; + ByteBuffer srcBuf = null, destBuf = null; + if (src.hasArray()) { + srcArr = src.array(); + srcOff += src.arrayOffset(); + } else { + assert src.isDirect(); + srcBuf = src; + } + if (dest.hasArray()) { + destArr = dest.array(); + destOff += dest.arrayOffset(); + } else { + assert dest.isDirect(); + destBuf = dest; + } + + final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( + ptr, srcArr, srcBuf, srcOff, srcLen, + destArr, destBuf, destOff, maxDestLen, acceleration); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } else { + LZ4Compressor safeCompressor = safeInstance; + if (safeCompressor == null) { + safeCompressor = LZ4Factory.safeInstance().fastCompressor(); + safeInstance = safeCompressor; + } + return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); + } + } + + /** + * Returns true if this compressor has been closed. + * + * @return true if closed + */ + public boolean isClosed() { + return statePtr.get() == 0; + } + + /** + * Closes this compressor and releases native resources. + * After calling this method, all compress methods will throw {@link IllegalStateException} + */ + @Override + public void close() { + long ptr = statePtr.getAndSet(0); + if (ptr != 0) { + LZ4JNI.LZ4_freeStream(ptr); + } + } + + @Override + public String toString() { + return "LZ4JNIFastResetCompressor[acceleration=" + acceleration + ", closed=" + isClosed() + "]"; + } +} diff --git a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java new file mode 100644 index 00000000..37f4a38b --- /dev/null +++ b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java @@ -0,0 +1,215 @@ +package net.jpountz.lz4; + +/* + * Copyright 2020 Adrien Grand and the lz4-java contributors. + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import static net.jpountz.lz4.LZ4Constants.DEFAULT_COMPRESSION_LEVEL; + +import net.jpountz.util.ByteBufferUtils; +import net.jpountz.util.SafeUtils; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +/** + * An optimized LZ4 HC compressor that uses native {@code LZ4_compress_HC_extStateHC_fastReset}. + *

+ * This compressor pre-allocates an {@code LZ4_streamHC_t} once and reuses it for every compression + * call through {@code LZ4_compress_HC_extStateHC_fastReset}. This avoids the expensive full + * state initialization that {@link LZ4HCJNICompressor LZ4_compress_HC()} performs on every call. + *

+ * Each compression call is independent, making the output identical to {@link LZ4HCJNICompressor} + * for the same compression level. + *

+ * Thread Safety: This class is NOT thread-safe. Each instance holds + * mutable native state and must be used by only one thread at a time. + *

+ * Resource Management: This class holds native memory that must be freed. + * Always use try-with-resources or explicitly call {@link #close()}. + * + *

Example usage:

+ *
{@code
+ * LZ4Factory factory = LZ4Factory.nativeInstance();
+ * try (LZ4JNIHCFastResetCompressor compressor = factory.highFastResetCompressor()) {
+ *     byte[] compressed = new byte[compressor.maxCompressedLength(data.length)];
+ *     int compressedLen = compressor.compress(data, 0, data.length, compressed, 0, compressed.length);
+ *     // ... use compressed[0..compressedLen-1]
+ * }
+ * }
+ * + * @see LZ4Factory#highFastResetCompressor() + * @see LZ4Factory#highFastResetCompressor(int) + * @see LZ4HCJNICompressor + */ +public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements AutoCloseable { + + private final AtomicLong statePtr; + private final int compressionLevel; + private LZ4Compressor safeInstance; + + /** + * Creates a new HC fast-reset compressor with default compression level. + * Package-private: use {@link LZ4Factory#highFastResetCompressor()}. + */ + LZ4JNIHCFastResetCompressor() { + this(DEFAULT_COMPRESSION_LEVEL); + } + + /** + * Creates a new HC fast-reset compressor with specified compression level. + * Package-private: use {@link LZ4Factory#highFastResetCompressor(int)}. + * + * @param compressionLevel compression level (1-17, higher = better compression) + */ + LZ4JNIHCFastResetCompressor(int compressionLevel) { + this.compressionLevel = compressionLevel; + long ptr = LZ4JNI.LZ4_createStreamHC(); + if (ptr == 0) { + throw new LZ4Exception("Failed to allocate LZ4 HC state"); + } + this.statePtr = new AtomicLong(ptr); + } + + /** + * Returns the compression level. + * + * @return compression level (default = 9) + */ + public int getCompressionLevel() { + return compressionLevel; + } + + /** + * Compresses {@code src[srcOff:srcOff+srcLen]} into + * {@code dest[destOff:destOff+maxDestLen]}. + * + * @param src source data + * @param srcOff the start offset in src + * @param srcLen the number of bytes to compress + * @param dest destination buffer + * @param destOff the start offset in dest + * @param maxDestLen the maximum number of bytes to write in dest + * @return the compressed size + * @throws LZ4Exception if maxDestLen is too small + * @throws IllegalStateException if the compressor has been closed + */ + public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { + long ptr = statePtr.get(); + if (ptr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + SafeUtils.checkRange(src, srcOff, srcLen); + SafeUtils.checkRange(dest, destOff, maxDestLen); + + final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( + ptr, src, null, srcOff, srcLen, + dest, null, destOff, maxDestLen, compressionLevel); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } + + /** + * Compresses {@code src[srcOff:srcOff+srcLen]} into + * {@code dest[destOff:destOff+maxDestLen]}. + *

+ * Both buffers must be either direct or array-backed. If neither, the method + * falls back to the safe Java compressor. + * {@link ByteBuffer} positions remain unchanged. + * + * @param src source data + * @param srcOff the start offset in src + * @param srcLen the number of bytes to compress + * @param dest destination buffer + * @param destOff the start offset in dest + * @param maxDestLen the maximum number of bytes to write in dest + * @return the compressed size + * @throws LZ4Exception if maxDestLen is too small + * @throws IllegalStateException if the compressor has been closed + */ + public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { + long ptr = statePtr.get(); + if (ptr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + ByteBufferUtils.checkNotReadOnly(dest); + ByteBufferUtils.checkRange(src, srcOff, srcLen); + ByteBufferUtils.checkRange(dest, destOff, maxDestLen); + + if ((src.hasArray() || src.isDirect()) && (dest.hasArray() || dest.isDirect())) { + byte[] srcArr = null, destArr = null; + ByteBuffer srcBuf = null, destBuf = null; + if (src.hasArray()) { + srcArr = src.array(); + srcOff += src.arrayOffset(); + } else { + assert src.isDirect(); + srcBuf = src; + } + if (dest.hasArray()) { + destArr = dest.array(); + destOff += dest.arrayOffset(); + } else { + assert dest.isDirect(); + destBuf = dest; + } + + final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( + ptr, srcArr, srcBuf, srcOff, srcLen, + destArr, destBuf, destOff, maxDestLen, compressionLevel); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } else { + LZ4Compressor safeCompressor = safeInstance; + if (safeCompressor == null) { + safeCompressor = LZ4Factory.safeInstance().highCompressor(compressionLevel); + safeInstance = safeCompressor; + } + return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); + } + } + + /** + * Returns true if this compressor has been closed. + * + * @return true if closed + */ + public boolean isClosed() { + return statePtr.get() == 0; + } + + /** + * Closes this compressor and releases native resources. + * After calling this method, all compress methods will throw {@link IllegalStateException} + */ + @Override + public void close() { + long ptr = statePtr.getAndSet(0); + if (ptr != 0) { + LZ4JNI.LZ4_freeStreamHC(ptr); + } + } + + @Override + public String toString() { + return "LZ4JNIHCFastResetCompressor[compressionLevel=" + compressionLevel + ", closed=" + isClosed() + "]"; + } +} diff --git a/src/jni/net_jpountz_lz4_LZ4JNI.c b/src/jni/net_jpountz_lz4_LZ4JNI.c index 3935b32a..82710eaf 100644 --- a/src/jni/net_jpountz_lz4_LZ4JNI.c +++ b/src/jni/net_jpountz_lz4_LZ4JNI.c @@ -14,11 +14,26 @@ * limitations under the License. */ +#define LZ4_STATIC_LINKING_ONLY // exposes LZ4_compress_fast_extState_fastReset #include "lz4.h" +#define LZ4_HC_STATIC_LINKING_ONLY // exposes LZ4_compress_HC_extStateHC_fastReset #include "lz4hc.h" #include static jclass OutOfMemoryError; +static jclass IllegalArgumentException; +static jclass IllegalStateException; + +static int init_class_ref(JNIEnv *env, jclass *target, const char *className) { + jclass localClass = (*env)->FindClass(env, className); + if (localClass == NULL) { + return 0; + } + + *target = (jclass) (*env)->NewGlobalRef(env, localClass); + (*env)->DeleteLocalRef(env, localClass); + return *target != NULL; +} /* * Class: net_jpountz_lz4_LZ4 @@ -27,13 +42,53 @@ static jclass OutOfMemoryError; */ JNIEXPORT void JNICALL Java_net_jpountz_lz4_LZ4JNI_init (JNIEnv *env, jclass cls) { - OutOfMemoryError = (*env)->FindClass(env, "java/lang/OutOfMemoryError"); + if (!init_class_ref(env, &OutOfMemoryError, "java/lang/OutOfMemoryError")) { + return; + } + if (!init_class_ref(env, &IllegalArgumentException, "java/lang/IllegalArgumentException")) { + return; + } + if (!init_class_ref(env, &IllegalStateException, "java/lang/IllegalStateException")) { + return; + } } static void throw_OOM(JNIEnv *env) { (*env)->ThrowNew(env, OutOfMemoryError, "Out of memory"); } +static void throw_IAE(JNIEnv *env, const char* msg) { + (*env)->ThrowNew(env, IllegalArgumentException, msg); +} + +static void throw_ISE(JNIEnv *env, const char* msg) { + (*env)->ThrowNew(env, IllegalStateException, msg); +} + +/** + * Validates that offset and length are non-negative and don't overflow. + * Returns 1 if valid, 0 otherwise (and throws exception). + */ +static int validate_range(JNIEnv *env, jint off, jint len) { + if (off < 0) { + throw_IAE(env, "Offset must be non-negative"); + return 0; + } + + if (len < 0) { + throw_IAE(env, "Length must be non-negative"); + return 0; + } + + // Check for integer overflow: off + len + if (len > 0 && off > INT32_MAX - len) { + throw_IAE(env, "Offset + length would overflow"); + return 0; + } + + return 1; +} + /* * Class: net_jpountz_lz4_LZ4JNI * Method: LZ4_compress_limitedOutput @@ -237,3 +292,181 @@ JNIEXPORT jint JNICALL Java_net_jpountz_lz4_LZ4JNI_LZ4_1compressBound return LZ4_compressBound(len); } + +/* + * Class: net_jpountz_lz4_LZ4JNI + * Method: LZ4_createStream + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_net_jpountz_lz4_LZ4JNI_LZ4_1createStream + (JNIEnv *env, jclass cls) { + + LZ4_stream_t* stream = LZ4_createStream(); + return (jlong)(intptr_t)stream; +} + +/* + * Class: net_jpountz_lz4_LZ4JNI + * Method: LZ4_freeStream + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_net_jpountz_lz4_LZ4JNI_LZ4_1freeStream + (JNIEnv *env, jclass cls, jlong streamPtr) { + + if (streamPtr == 0) { + return 0; + } + + LZ4_stream_t* stream = (LZ4_stream_t*)(intptr_t)streamPtr; + return LZ4_freeStream(stream); +} + +/* + * Class: net_jpountz_lz4_LZ4JNI + * Method: LZ4_compress_fast_extState_fastReset + * Signature: (J[BLjava/nio/ByteBuffer;II[BLjava/nio/ByteBuffer;III)I + */ +JNIEXPORT jint JNICALL Java_net_jpountz_lz4_LZ4JNI_LZ4_1compress_1fast_1extState_1fastReset + (JNIEnv *env, jclass cls, jlong statePtr, + jbyteArray srcArray, jobject srcBuffer, jint srcOff, jint srcLen, + jbyteArray destArray, jobject destBuffer, jint destOff, jint maxDestLen, jint acceleration) { + + if (statePtr == 0) { + throw_ISE(env, "Compressor state has been freed"); + return 0; + } + + if (!validate_range(env, srcOff, srcLen) || !validate_range(env, destOff, maxDestLen)) { + return 0; + } + + void* state = (void*)(intptr_t)statePtr; + char* in; + char* out; + jint compressed; + + if (srcArray != NULL) { + in = (char*) (*env)->GetPrimitiveArrayCritical(env, srcArray, 0); + } else { + in = (char*) (*env)->GetDirectBufferAddress(env, srcBuffer); + } + + if (in == NULL) { + throw_OOM(env); + return 0; + } + + if (destArray != NULL) { + out = (char*) (*env)->GetPrimitiveArrayCritical(env, destArray, 0); + } else { + out = (char*) (*env)->GetDirectBufferAddress(env, destBuffer); + } + + if (out == NULL) { + if (srcArray != NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, srcArray, in, 0); + } + throw_OOM(env); + return 0; + } + + compressed = LZ4_compress_fast_extState_fastReset(state, in + srcOff, out + destOff, srcLen, maxDestLen, acceleration); + + if (srcArray != NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, srcArray, in, 0); + } + if (destArray != NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, destArray, out, 0); + } + + return compressed; +} + +/* + * Class: net_jpountz_lz4_LZ4JNI + * Method: LZ4_createStreamHC + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_net_jpountz_lz4_LZ4JNI_LZ4_1createStreamHC + (JNIEnv *env, jclass cls) { + + LZ4_streamHC_t* stream = LZ4_createStreamHC(); + return (jlong)(intptr_t)stream; +} + +/* + * Class: net_jpountz_lz4_LZ4JNI + * Method: LZ4_freeStreamHC + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_net_jpountz_lz4_LZ4JNI_LZ4_1freeStreamHC + (JNIEnv *env, jclass cls, jlong streamPtr) { + + if (streamPtr == 0) { + return 0; + } + + LZ4_streamHC_t* stream = (LZ4_streamHC_t*)(intptr_t)streamPtr; + return LZ4_freeStreamHC(stream); +} + +/* + * Class: net_jpountz_lz4_LZ4JNI + * Method: LZ4_compress_HC_extStateHC_fastReset + * Signature: (J[BLjava/nio/ByteBuffer;II[BLjava/nio/ByteBuffer;III)I + */ +JNIEXPORT jint JNICALL Java_net_jpountz_lz4_LZ4JNI_LZ4_1compress_1HC_1extStateHC_1fastReset + (JNIEnv *env, jclass cls, jlong statePtr, + jbyteArray srcArray, jobject srcBuffer, jint srcOff, jint srcLen, + jbyteArray destArray, jobject destBuffer, jint destOff, jint maxDestLen, jint compressionLevel) { + + if (statePtr == 0) { + throw_ISE(env, "Compressor state has been freed"); + return 0; + } + + if (!validate_range(env, srcOff, srcLen) || !validate_range(env, destOff, maxDestLen)) { + return 0; + } + + void* state = (void*)(intptr_t)statePtr; + char* in; + char* out; + jint compressed; + + if (srcArray != NULL) { + in = (char*) (*env)->GetPrimitiveArrayCritical(env, srcArray, 0); + } else { + in = (char*) (*env)->GetDirectBufferAddress(env, srcBuffer); + } + + if (in == NULL) { + throw_OOM(env); + return 0; + } + + if (destArray != NULL) { + out = (char*) (*env)->GetPrimitiveArrayCritical(env, destArray, 0); + } else { + out = (char*) (*env)->GetDirectBufferAddress(env, destBuffer); + } + + if (out == NULL) { + if (srcArray != NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, srcArray, in, 0); + } + throw_OOM(env); + return 0; + } + + compressed = LZ4_compress_HC_extStateHC_fastReset(state, in + srcOff, out + destOff, srcLen, maxDestLen, compressionLevel); + + if (srcArray != NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, srcArray, in, 0); + } + if (destArray != NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, destArray, out, 0); + } + + return compressed; +} From c01b7f4bda9a7e803bf247c9babe45cf4b3251d1 Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Fri, 3 Apr 2026 13:20:07 +0300 Subject: [PATCH 2/9] Add native fast reset compressor tests --- pom.xml | 106 +++++++++++ .../net/jpountz/fuzz/LZ4CompressorTest.java | 106 ++++++++++- .../net/jpountz/lz4/LZ4FastResetTest.java | 167 ++++++++++++++++++ 3 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 src/test/net/jpountz/lz4/LZ4FastResetTest.java diff --git a/pom.xml b/pom.xml index 87388718..d54d87d9 100644 --- a/pom.xml +++ b/pom.xml @@ -950,6 +950,112 @@ + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_array + test + + net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_array + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a9_array + test + + net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a9_array + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a17_array + test + + net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a17_array + + + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_bytebuffer + test + + net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_bytebuffer + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a9_bytebuffer + test + + net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a9_bytebuffer + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a17_bytebuffer + test + + net.jpountz.fuzz.LZ4CompressorTest#native_fastReset_a17_bytebuffer + + + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_array + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_array + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l1_array + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l1_array + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l9_array + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l9_array + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l17_array + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l17_array + + + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_bytebuffer + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_bytebuffer + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l1_bytebuffer + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l1_bytebuffer + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l9_bytebuffer + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l9_bytebuffer + + + + fuzz-net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l17_bytebuffer + test + + net.jpountz.fuzz.LZ4CompressorTest#native_highFastReset_l17_bytebuffer + + + fuzz-net.jpountz.fuzz.XXHash32Test#safe_array diff --git a/src/test/net/jpountz/fuzz/LZ4CompressorTest.java b/src/test/net/jpountz/fuzz/LZ4CompressorTest.java index 8b1b588b..56122d57 100644 --- a/src/test/net/jpountz/fuzz/LZ4CompressorTest.java +++ b/src/test/net/jpountz/fuzz/LZ4CompressorTest.java @@ -2,9 +2,7 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.code_intelligence.jazzer.junit.FuzzTest; -import net.jpountz.lz4.LZ4Compressor; -import net.jpountz.lz4.LZ4Exception; -import net.jpountz.lz4.LZ4Factory; +import net.jpountz.lz4.*; import java.nio.ByteBuffer; @@ -177,4 +175,106 @@ public void unsafe_high_l17_bytebuffer(FuzzedDataProvider provider) { public void native_high_l17_bytebuffer(FuzzedDataProvider provider) { testByteBuffer(provider, LZ4Factory.nativeInsecureInstance().highCompressor(17)); } + + // fastResetCompressor: array + @FuzzTest + public void native_fastReset_array(FuzzedDataProvider provider) { + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor()) { + testArray(provider, compressor); + } + } + + @FuzzTest + public void native_fastReset_a9_array(FuzzedDataProvider provider) { + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor(9)) { + testArray(provider, compressor); + } + } + + @FuzzTest + public void native_fastReset_a17_array(FuzzedDataProvider provider) { + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor(17)) { + testArray(provider, compressor); + } + } + + // fastResetCompressor: bytebuffer + @FuzzTest + public void native_fastReset_bytebuffer(FuzzedDataProvider provider) { + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor()) { + testByteBuffer(provider, compressor); + } + } + + @FuzzTest + public void native_fastReset_a9_bytebuffer(FuzzedDataProvider provider) { + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor(9)) { + testByteBuffer(provider, compressor); + } + } + + @FuzzTest + public void native_fastReset_a17_bytebuffer(FuzzedDataProvider provider) { + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor(17)) { + testByteBuffer(provider, compressor); + } + } + + // highFastResetCompressor: array + @FuzzTest + public void native_highFastReset_array(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor()) { + testArray(provider, compressor); + } + } + + @FuzzTest + public void native_highFastReset_l1_array(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor(1)) { + testArray(provider, compressor); + } + } + + @FuzzTest + public void native_highFastReset_l9_array(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor(9)) { + testArray(provider, compressor); + } + } + + @FuzzTest + public void native_highFastReset_l17_array(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor(17)) { + testArray(provider, compressor); + } + } + + // highFastResetCompressor: bytebuffer + @FuzzTest + public void native_highFastReset_bytebuffer(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor()) { + testByteBuffer(provider, compressor); + } + } + + @FuzzTest + public void native_highFastReset_l1_bytebuffer(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor(1)) { + testByteBuffer(provider, compressor); + } + } + + @FuzzTest + public void native_highFastReset_l9_bytebuffer(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor(9)) { + testByteBuffer(provider, compressor); + } + } + + @FuzzTest + public void native_highFastReset_l17_bytebuffer(FuzzedDataProvider provider) { + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor(17)) { + testByteBuffer(provider, compressor); + } + } } diff --git a/src/test/net/jpountz/lz4/LZ4FastResetTest.java b/src/test/net/jpountz/lz4/LZ4FastResetTest.java new file mode 100644 index 00000000..30dc0d79 --- /dev/null +++ b/src/test/net/jpountz/lz4/LZ4FastResetTest.java @@ -0,0 +1,167 @@ +package net.jpountz.lz4; + +/* + * Copyright 2020 Adrien Grand and the lz4-java contributors. + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import junit.framework.TestCase; + +import java.lang.reflect.Field; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertArrayEquals; + +public class LZ4FastResetTest extends TestCase { + + public void testFastResetCompressorLifecycleAndRoundTrip() { + LZ4Factory factory = LZ4Factory.nativeInstance(); + byte[] data = repeatedData(); + + try (LZ4JNIFastResetCompressor compressor = factory.fastResetCompressor()) { + assertEquals(LZ4Constants.DEFAULT_ACCELERATION, compressor.getAcceleration()); + assertFalse(compressor.isClosed()); + + byte[] compressed = new byte[compressor.maxCompressedLength(data.length)]; + int compressedLength = compressor.compress(data, 0, data.length, compressed, 0, compressed.length); + byte[] restored = new byte[data.length]; + + int restoredLength = factory.safeDecompressor().decompress(compressed, 0, compressedLength, restored, 0); + assertEquals(data.length, restoredLength); + assertArrayEquals(data, restored); + + compressor.close(); + assertTrue(compressor.isClosed()); + compressor.close(); + + try { + compressor.compress(data, 0, data.length, compressed, 0, compressed.length); + fail(); + } catch (IllegalStateException expected) { + // expected + } + } + } + + public void testFastResetAccelerationNormalization() { + LZ4Factory factory = LZ4Factory.nativeInstance(); + + try (LZ4JNIFastResetCompressor defaultCompressor = factory.fastResetCompressor(); + LZ4JNIFastResetCompressor negativeCompressor = factory.fastResetCompressor(-17); + LZ4JNIFastResetCompressor maxCompressor = factory.fastResetCompressor(Integer.MAX_VALUE)) { + assertEquals(LZ4Constants.DEFAULT_ACCELERATION, defaultCompressor.getAcceleration()); + assertEquals(LZ4Constants.MIN_ACCELERATION, negativeCompressor.getAcceleration()); + assertEquals(LZ4Constants.MAX_ACCELERATION, maxCompressor.getAcceleration()); + } + } + + public void testHighFastResetCompressorLifecycleAndLevelNormalization() { + LZ4Factory factory = LZ4Factory.nativeInstance(); + byte[] data = repeatedData(); + + try (LZ4JNIHCFastResetCompressor defaultCompressor = factory.highFastResetCompressor(); + LZ4JNIHCFastResetCompressor lowCompressor = factory.highFastResetCompressor(0); + LZ4JNIHCFastResetCompressor highCompressor = factory.highFastResetCompressor(99)) { + assertEquals(LZ4Constants.DEFAULT_COMPRESSION_LEVEL, defaultCompressor.getCompressionLevel()); + assertEquals(LZ4Constants.DEFAULT_COMPRESSION_LEVEL, lowCompressor.getCompressionLevel()); + assertEquals(LZ4Constants.MAX_COMPRESSION_LEVEL, highCompressor.getCompressionLevel()); + + byte[] compressed = new byte[highCompressor.maxCompressedLength(data.length)]; + int compressedLength = highCompressor.compress(data, 0, data.length, compressed, 0, compressed.length); + byte[] restored = new byte[data.length]; + + int restoredLength = factory.safeDecompressor().decompress(compressed, 0, compressedLength, restored, 0); + assertEquals(data.length, restoredLength); + assertArrayEquals(data, restored); + + highCompressor.close(); + assertTrue(highCompressor.isClosed()); + highCompressor.close(); + + try { + highCompressor.compress(data, 0, data.length, compressed, 0, compressed.length); + fail(); + } catch (IllegalStateException expected) { + // expected + } + } + } + + public void testFastResetByteBufferFallbackUsesSafeJavaCompressor() throws Exception { + LZ4Factory factory = LZ4Factory.nativeInstance(); + ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); + int maxCompressedLength = LZ4Utils.maxCompressedLength(src.remaining()); + + try (LZ4JNIFastResetCompressor defaultCompressor = factory.fastResetCompressor(); + LZ4JNIFastResetCompressor acceleratedCompressor = factory.fastResetCompressor(9)) { + defaultCompressor.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); + acceleratedCompressor.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); + + Field safeInstanceField = LZ4JNIFastResetCompressor.class.getDeclaredField("safeInstance"); + safeInstanceField.setAccessible(true); + + assertSame(LZ4Factory.safeInstance().fastCompressor(), safeInstanceField.get(defaultCompressor)); + assertSame(LZ4Factory.safeInstance().fastCompressor(), safeInstanceField.get(acceleratedCompressor)); + } + } + + public void testHighFastResetByteBufferFallbackKeepsCompressionLevel() throws Exception { + LZ4Factory factory = LZ4Factory.nativeInstance(); + ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); + int maxCompressedLength = LZ4Utils.maxCompressedLength(src.remaining()); + + try (LZ4JNIHCFastResetCompressor level1 = factory.highFastResetCompressor(1); + LZ4JNIHCFastResetCompressor level17 = factory.highFastResetCompressor(17)) { + level1.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); + level17.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); + + Field safeInstanceField = LZ4JNIHCFastResetCompressor.class.getDeclaredField("safeInstance"); + safeInstanceField.setAccessible(true); + + assertSame(LZ4Factory.safeInstance().highCompressor(1), safeInstanceField.get(level1)); + assertSame(LZ4Factory.safeInstance().highCompressor(17), safeInstanceField.get(level17)); + } + } + + public void testFastResetMethodsRequireNativeFactory() { + assertFastResetUnsupported(LZ4Factory.safeInstance()); + assertFastResetUnsupported(LZ4Factory.unsafeInstance()); + assertFastResetUnsupported(LZ4Factory.unsafeInsecureInstance()); + } + + private static byte[] repeatedData() { + byte[] data = new byte[1024]; + for (int i = 0; i < data.length; ++i) { + data[i] = (byte) ('a' + (i % 7)); + } + return data; + } + + private static void assertFastResetUnsupported(LZ4Factory factory) { + try { + factory.fastResetCompressor(); + fail(); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + factory.highFastResetCompressor(); + fail(); + } catch (UnsupportedOperationException expected) { + // expected + } + } +} + From ad9a637ab06c20eb8a8cbd64fa06cf3eac48b485 Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Fri, 3 Apr 2026 13:38:59 +0300 Subject: [PATCH 3/9] Simplify byte buffer paths --- .../lz4/LZ4JNIFastResetCompressor.java | 56 +++++++++---------- .../lz4/LZ4JNIHCFastResetCompressor.java | 56 +++++++++---------- .../net/jpountz/lz4/LZ4FastResetTest.java | 15 +++++ 3 files changed, 71 insertions(+), 56 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java index bcf992d6..9c81a1ce 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java @@ -33,7 +33,8 @@ * state initialization that {@link LZ4JNICompressor LZ4_compress_default()} performs on every call. *

* Each compression call is independent, making the output identical to {@link LZ4JNICompressor} - * for the same acceleration level. + * for the same acceleration level when operating on array-backed or direct buffers. + * ByteBuffer inputs that are neither array-backed nor direct fall back to the safe Java compressor. *

* Thread Safety: This class is NOT thread-safe. Each instance holds * mutable native state and must be used by only one thread at a time. @@ -157,33 +158,7 @@ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int ByteBufferUtils.checkRange(src, srcOff, srcLen); ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - if ((src.hasArray() || src.isDirect()) && (dest.hasArray() || dest.isDirect())) { - byte[] srcArr = null, destArr = null; - ByteBuffer srcBuf = null, destBuf = null; - if (src.hasArray()) { - srcArr = src.array(); - srcOff += src.arrayOffset(); - } else { - assert src.isDirect(); - srcBuf = src; - } - if (dest.hasArray()) { - destArr = dest.array(); - destOff += dest.arrayOffset(); - } else { - assert dest.isDirect(); - destBuf = dest; - } - - final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( - ptr, srcArr, srcBuf, srcOff, srcLen, - destArr, destBuf, destOff, maxDestLen, acceleration); - - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); - } - return result; - } else { + if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { LZ4Compressor safeCompressor = safeInstance; if (safeCompressor == null) { safeCompressor = LZ4Factory.safeInstance().fastCompressor(); @@ -191,6 +166,31 @@ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int } return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); } + + return compressNativeBuffers(ptr, src, srcOff, srcLen, dest, destOff, maxDestLen); + } + + private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcLen, + ByteBuffer dest, int destOff, int maxDestLen) { + byte[] srcArr = src.hasArray() ? src.array() : null; + byte[] destArr = dest.hasArray() ? dest.array() : null; + ByteBuffer srcBuf = srcArr == null ? src : null; + ByteBuffer destBuf = destArr == null ? dest : null; + int srcBufferOff = srcOff + (srcArr != null ? src.arrayOffset() : 0); + int destBufferOff = destOff + (destArr != null ? dest.arrayOffset() : 0); + + final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( + ptr, srcArr, srcBuf, srcBufferOff, srcLen, + destArr, destBuf, destBufferOff, maxDestLen, acceleration); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } + + private static boolean hasCompatibleBacking(ByteBuffer buffer) { + return buffer.hasArray() || buffer.isDirect(); } /** diff --git a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java index 37f4a38b..736bbc7c 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java @@ -32,7 +32,8 @@ * state initialization that {@link LZ4HCJNICompressor LZ4_compress_HC()} performs on every call. *

* Each compression call is independent, making the output identical to {@link LZ4HCJNICompressor} - * for the same compression level. + * for the same compression level when operating on array-backed or direct buffers. + * ByteBuffer inputs that are neither array-backed nor direct fall back to the safe Java compressor. *

* Thread Safety: This class is NOT thread-safe. Each instance holds * mutable native state and must be used by only one thread at a time. @@ -151,33 +152,7 @@ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int ByteBufferUtils.checkRange(src, srcOff, srcLen); ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - if ((src.hasArray() || src.isDirect()) && (dest.hasArray() || dest.isDirect())) { - byte[] srcArr = null, destArr = null; - ByteBuffer srcBuf = null, destBuf = null; - if (src.hasArray()) { - srcArr = src.array(); - srcOff += src.arrayOffset(); - } else { - assert src.isDirect(); - srcBuf = src; - } - if (dest.hasArray()) { - destArr = dest.array(); - destOff += dest.arrayOffset(); - } else { - assert dest.isDirect(); - destBuf = dest; - } - - final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( - ptr, srcArr, srcBuf, srcOff, srcLen, - destArr, destBuf, destOff, maxDestLen, compressionLevel); - - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); - } - return result; - } else { + if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { LZ4Compressor safeCompressor = safeInstance; if (safeCompressor == null) { safeCompressor = LZ4Factory.safeInstance().highCompressor(compressionLevel); @@ -185,6 +160,31 @@ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int } return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); } + + return compressNativeBuffers(ptr, src, srcOff, srcLen, dest, destOff, maxDestLen); + } + + private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcLen, + ByteBuffer dest, int destOff, int maxDestLen) { + byte[] srcArr = src.hasArray() ? src.array() : null; + byte[] destArr = dest.hasArray() ? dest.array() : null; + ByteBuffer srcBuf = srcArr == null ? src : null; + ByteBuffer destBuf = destArr == null ? dest : null; + int srcBufferOff = srcOff + (srcArr != null ? src.arrayOffset() : 0); + int destBufferOff = destOff + (destArr != null ? dest.arrayOffset() : 0); + + final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( + ptr, srcArr, srcBuf, srcBufferOff, srcLen, + destArr, destBuf, destBufferOff, maxDestLen, compressionLevel); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } + + private static boolean hasCompatibleBacking(ByteBuffer buffer) { + return buffer.hasArray() || buffer.isDirect(); } /** diff --git a/src/test/net/jpountz/lz4/LZ4FastResetTest.java b/src/test/net/jpountz/lz4/LZ4FastResetTest.java index 30dc0d79..7ba3c10a 100644 --- a/src/test/net/jpountz/lz4/LZ4FastResetTest.java +++ b/src/test/net/jpountz/lz4/LZ4FastResetTest.java @@ -116,6 +116,21 @@ public void testFastResetByteBufferFallbackUsesSafeJavaCompressor() throws Excep } } + public void testFastResetByteBufferAfterClose() { + ByteBuffer src = ByteBuffer.wrap(repeatedData()); + ByteBuffer dest = ByteBuffer.allocate(LZ4Utils.maxCompressedLength(src.remaining())); + + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor()) { + compressor.close(); + try { + compressor.compress(src, 0, src.remaining(), dest, 0, dest.remaining()); + fail(); + } catch (IllegalStateException expected) { + // expected + } + } + } + public void testHighFastResetByteBufferFallbackKeepsCompressionLevel() throws Exception { LZ4Factory factory = LZ4Factory.nativeInstance(); ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); From 3661c7c73a76565297546f9d8f0f086e56fea016 Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Fri, 3 Apr 2026 13:39:38 +0300 Subject: [PATCH 4/9] Add constructor normalization for compression level and acceleration --- .../jpountz/lz4/LZ4JNIHCFastResetCompressor.java | 6 ++++++ src/test/net/jpountz/lz4/LZ4FastResetTest.java | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java index 736bbc7c..ac210049 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java @@ -17,6 +17,7 @@ */ import static net.jpountz.lz4.LZ4Constants.DEFAULT_COMPRESSION_LEVEL; +import static net.jpountz.lz4.LZ4Constants.MAX_COMPRESSION_LEVEL; import net.jpountz.util.ByteBufferUtils; import net.jpountz.util.SafeUtils; @@ -76,6 +77,11 @@ public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements * @param compressionLevel compression level (1-17, higher = better compression) */ LZ4JNIHCFastResetCompressor(int compressionLevel) { + if (compressionLevel > MAX_COMPRESSION_LEVEL) { + compressionLevel = MAX_COMPRESSION_LEVEL; + } else if (compressionLevel < 1) { + compressionLevel = DEFAULT_COMPRESSION_LEVEL; + } this.compressionLevel = compressionLevel; long ptr = LZ4JNI.LZ4_createStreamHC(); if (ptr == 0) { diff --git a/src/test/net/jpountz/lz4/LZ4FastResetTest.java b/src/test/net/jpountz/lz4/LZ4FastResetTest.java index 7ba3c10a..00a2d345 100644 --- a/src/test/net/jpountz/lz4/LZ4FastResetTest.java +++ b/src/test/net/jpountz/lz4/LZ4FastResetTest.java @@ -66,6 +66,14 @@ public void testFastResetAccelerationNormalization() { } } + public void testFastResetConstructorNormalization() { + try (LZ4JNIFastResetCompressor negativeCompressor = new LZ4JNIFastResetCompressor(-17); + LZ4JNIFastResetCompressor maxCompressor = new LZ4JNIFastResetCompressor(Integer.MAX_VALUE)) { + assertEquals(LZ4Constants.MIN_ACCELERATION, negativeCompressor.getAcceleration()); + assertEquals(LZ4Constants.MAX_ACCELERATION, maxCompressor.getAcceleration()); + } + } + public void testHighFastResetCompressorLifecycleAndLevelNormalization() { LZ4Factory factory = LZ4Factory.nativeInstance(); byte[] data = repeatedData(); @@ -98,6 +106,14 @@ public void testHighFastResetCompressorLifecycleAndLevelNormalization() { } } + public void testHighFastResetConstructorNormalization() { + try (LZ4JNIHCFastResetCompressor lowCompressor = new LZ4JNIHCFastResetCompressor(0); + LZ4JNIHCFastResetCompressor highCompressor = new LZ4JNIHCFastResetCompressor(99)) { + assertEquals(LZ4Constants.DEFAULT_COMPRESSION_LEVEL, lowCompressor.getCompressionLevel()); + assertEquals(LZ4Constants.MAX_COMPRESSION_LEVEL, highCompressor.getCompressionLevel()); + } + } + public void testFastResetByteBufferFallbackUsesSafeJavaCompressor() throws Exception { LZ4Factory factory = LZ4Factory.nativeInstance(); ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); From 47c6557e1d8ef7e778010eacb781845cc7df7759 Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Wed, 8 Apr 2026 18:43:50 +0300 Subject: [PATCH 5/9] Switch to a lock for guarding the statePtr --- .../lz4/LZ4JNIFastResetCompressor.java | 92 ++++++++++++------- .../lz4/LZ4JNIHCFastResetCompressor.java | 92 ++++++++++++------- 2 files changed, 118 insertions(+), 66 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java index 9c81a1ce..90f354ec 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java @@ -20,7 +20,7 @@ import net.jpountz.util.SafeUtils; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; import static net.jpountz.lz4.LZ4Constants.MIN_ACCELERATION; import static net.jpountz.lz4.LZ4Constants.MAX_ACCELERATION; @@ -58,7 +58,8 @@ */ public final class LZ4JNIFastResetCompressor extends LZ4Compressor implements AutoCloseable { - private final AtomicLong statePtr; + private final ReentrantLock lock = new ReentrantLock(); + private long statePtr; private final int acceleration; private LZ4Compressor safeInstance; @@ -87,7 +88,7 @@ public final class LZ4JNIFastResetCompressor extends LZ4Compressor implements Au if (ptr == 0) { throw new LZ4Exception("Failed to allocate LZ4 state"); } - this.statePtr = new AtomicLong(ptr); + this.statePtr = ptr; } /** @@ -114,21 +115,28 @@ public int getAcceleration() { * @throws IllegalStateException if the compressor has been closed */ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { - long ptr = statePtr.get(); - if (ptr == 0) { - throw new IllegalStateException("Compressor has been closed"); + if (!lock.tryLock()) { + throw new IllegalStateException("This compressor is not thread-safe and is already in use"); } - SafeUtils.checkRange(src, srcOff, srcLen); - SafeUtils.checkRange(dest, destOff, maxDestLen); - final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( - ptr, src, null, srcOff, srcLen, - dest, null, destOff, maxDestLen, acceleration); + try { + if (statePtr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + SafeUtils.checkRange(src, srcOff, srcLen); + SafeUtils.checkRange(dest, destOff, maxDestLen); - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); + final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( + statePtr, src, null, srcOff, srcLen, + dest, null, destOff, maxDestLen, acceleration); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } finally { + lock.unlock(); } - return result; } /** @@ -150,24 +158,31 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff * @throws IllegalStateException if the compressor has been closed */ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { - long ptr = statePtr.get(); - if (ptr == 0) { - throw new IllegalStateException("Compressor has been closed"); + if (!lock.tryLock()) { + throw new IllegalStateException("This compressor is not thread-safe and is already in use"); } - ByteBufferUtils.checkNotReadOnly(dest); - ByteBufferUtils.checkRange(src, srcOff, srcLen); - ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - - if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { - LZ4Compressor safeCompressor = safeInstance; - if (safeCompressor == null) { - safeCompressor = LZ4Factory.safeInstance().fastCompressor(); - safeInstance = safeCompressor; + + try { + if (statePtr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + ByteBufferUtils.checkNotReadOnly(dest); + ByteBufferUtils.checkRange(src, srcOff, srcLen); + ByteBufferUtils.checkRange(dest, destOff, maxDestLen); + + if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { + LZ4Compressor safeCompressor = safeInstance; + if (safeCompressor == null) { + safeCompressor = LZ4Factory.safeInstance().fastCompressor(); + safeInstance = safeCompressor; + } + return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); } - return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); - } - return compressNativeBuffers(ptr, src, srcOff, srcLen, dest, destOff, maxDestLen); + return compressNativeBuffers(statePtr, src, srcOff, srcLen, dest, destOff, maxDestLen); + } finally { + lock.unlock(); + } } private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcLen, @@ -199,7 +214,12 @@ private static boolean hasCompatibleBacking(ByteBuffer buffer) { * @return true if closed */ public boolean isClosed() { - return statePtr.get() == 0; + lock.lock(); + try { + return statePtr == 0; + } finally { + lock.unlock(); + } } /** @@ -208,9 +228,15 @@ public boolean isClosed() { */ @Override public void close() { - long ptr = statePtr.getAndSet(0); - if (ptr != 0) { - LZ4JNI.LZ4_freeStream(ptr); + lock.lock(); + try { + long ptr = statePtr; + statePtr = 0; + if (ptr != 0) { + LZ4JNI.LZ4_freeStream(ptr); + } + } finally { + lock.unlock(); } } diff --git a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java index ac210049..4263c499 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java @@ -23,7 +23,7 @@ import net.jpountz.util.SafeUtils; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; /** * An optimized LZ4 HC compressor that uses native {@code LZ4_compress_HC_extStateHC_fastReset}. @@ -58,7 +58,8 @@ */ public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements AutoCloseable { - private final AtomicLong statePtr; + private final ReentrantLock lock = new ReentrantLock(); + private long statePtr; private final int compressionLevel; private LZ4Compressor safeInstance; @@ -87,7 +88,7 @@ public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements if (ptr == 0) { throw new LZ4Exception("Failed to allocate LZ4 HC state"); } - this.statePtr = new AtomicLong(ptr); + this.statePtr = ptr; } /** @@ -114,21 +115,28 @@ public int getCompressionLevel() { * @throws IllegalStateException if the compressor has been closed */ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { - long ptr = statePtr.get(); - if (ptr == 0) { - throw new IllegalStateException("Compressor has been closed"); + if (!lock.tryLock()) { + throw new IllegalStateException("This compressor is not thread-safe and is already in use"); } - SafeUtils.checkRange(src, srcOff, srcLen); - SafeUtils.checkRange(dest, destOff, maxDestLen); - final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( - ptr, src, null, srcOff, srcLen, - dest, null, destOff, maxDestLen, compressionLevel); + try { + if (statePtr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + SafeUtils.checkRange(src, srcOff, srcLen); + SafeUtils.checkRange(dest, destOff, maxDestLen); - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); + final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( + statePtr, src, null, srcOff, srcLen, + dest, null, destOff, maxDestLen, compressionLevel); + + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } finally { + lock.unlock(); } - return result; } /** @@ -150,24 +158,31 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff * @throws IllegalStateException if the compressor has been closed */ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { - long ptr = statePtr.get(); - if (ptr == 0) { - throw new IllegalStateException("Compressor has been closed"); + if (!lock.tryLock()) { + throw new IllegalStateException("This compressor is not thread-safe and is already in use"); } - ByteBufferUtils.checkNotReadOnly(dest); - ByteBufferUtils.checkRange(src, srcOff, srcLen); - ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - - if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { - LZ4Compressor safeCompressor = safeInstance; - if (safeCompressor == null) { - safeCompressor = LZ4Factory.safeInstance().highCompressor(compressionLevel); - safeInstance = safeCompressor; + + try { + if (statePtr == 0) { + throw new IllegalStateException("Compressor has been closed"); + } + ByteBufferUtils.checkNotReadOnly(dest); + ByteBufferUtils.checkRange(src, srcOff, srcLen); + ByteBufferUtils.checkRange(dest, destOff, maxDestLen); + + if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { + LZ4Compressor safeCompressor = safeInstance; + if (safeCompressor == null) { + safeCompressor = LZ4Factory.safeInstance().highCompressor(compressionLevel); + safeInstance = safeCompressor; + } + return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); } - return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); - } - return compressNativeBuffers(ptr, src, srcOff, srcLen, dest, destOff, maxDestLen); + return compressNativeBuffers(statePtr, src, srcOff, srcLen, dest, destOff, maxDestLen); + } finally { + lock.unlock(); + } } private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcLen, @@ -199,7 +214,12 @@ private static boolean hasCompatibleBacking(ByteBuffer buffer) { * @return true if closed */ public boolean isClosed() { - return statePtr.get() == 0; + lock.lock(); + try { + return statePtr == 0; + } finally { + lock.unlock(); + } } /** @@ -208,9 +228,15 @@ public boolean isClosed() { */ @Override public void close() { - long ptr = statePtr.getAndSet(0); - if (ptr != 0) { - LZ4JNI.LZ4_freeStreamHC(ptr); + lock.lock(); + try { + long ptr = statePtr; + statePtr = 0; + if (ptr != 0) { + LZ4JNI.LZ4_freeStreamHC(ptr); + } + } finally { + lock.unlock(); } } From 976efdfd80ee948fed51d3b7f430f9b7be16c6e5 Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Wed, 8 Apr 2026 18:44:00 +0300 Subject: [PATCH 6/9] Add missing HC test --- src/test/net/jpountz/lz4/LZ4FastResetTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/net/jpountz/lz4/LZ4FastResetTest.java b/src/test/net/jpountz/lz4/LZ4FastResetTest.java index 00a2d345..f9aaa464 100644 --- a/src/test/net/jpountz/lz4/LZ4FastResetTest.java +++ b/src/test/net/jpountz/lz4/LZ4FastResetTest.java @@ -165,6 +165,21 @@ public void testHighFastResetByteBufferFallbackKeepsCompressionLevel() throws Ex } } + public void testHighFastResetByteBufferAfterClose() { + ByteBuffer src = ByteBuffer.wrap(repeatedData()); + ByteBuffer dest = ByteBuffer.allocate(LZ4Utils.maxCompressedLength(src.remaining())); + + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor()) { + compressor.close(); + try { + compressor.compress(src, 0, src.remaining(), dest, 0, dest.remaining()); + fail(); + } catch (IllegalStateException expected) { + // expected + } + } + } + public void testFastResetMethodsRequireNativeFactory() { assertFastResetUnsupported(LZ4Factory.safeInstance()); assertFastResetUnsupported(LZ4Factory.unsafeInstance()); From ac259543fa4cd4a198217aa0cae06055b1e82f6d Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Thu, 9 Apr 2026 09:35:51 +0300 Subject: [PATCH 7/9] Throw on concurrent close attempt --- .../lz4/LZ4JNIFastResetCompressor.java | 26 +++++++++++++------ .../lz4/LZ4JNIHCFastResetCompressor.java | 26 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java index 90f354ec..9be2dafa 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java @@ -37,7 +37,8 @@ * ByteBuffer inputs that are neither array-backed nor direct fall back to the safe Java compressor. *

* Thread Safety: This class is NOT thread-safe. Each instance holds - * mutable native state and must be used by only one thread at a time. + * mutable native state and must be used by only one thread at a time. Concurrent use + * or close attempts fail fast with {@link IllegalStateException}. *

* Resource Management: This class holds native memory that must be freed. * Always use try-with-resources or explicitly call {@link #close()}. @@ -58,6 +59,10 @@ */ public final class LZ4JNIFastResetCompressor extends LZ4Compressor implements AutoCloseable { + private static final String IN_USE_ERROR = "This compressor is not thread-safe and is already in use"; + private static final String CLOSED_ERROR = "Compressor has been closed"; + private static final String UNSUPPORTED_BUFFER_ERROR = "ByteBuffer must be direct or array-backed"; + private final ReentrantLock lock = new ReentrantLock(); private long statePtr; private final int acceleration; @@ -112,16 +117,16 @@ public int getAcceleration() { * @param maxDestLen the maximum number of bytes to write in dest * @return the compressed size * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalStateException if the compressor has been closed + * @throws IllegalStateException if the compressor has been closed or is already in use */ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { if (!lock.tryLock()) { - throw new IllegalStateException("This compressor is not thread-safe and is already in use"); + throw new IllegalStateException(IN_USE_ERROR); } try { if (statePtr == 0) { - throw new IllegalStateException("Compressor has been closed"); + throw new IllegalStateException(CLOSED_ERROR); } SafeUtils.checkRange(src, srcOff, srcLen); SafeUtils.checkRange(dest, destOff, maxDestLen); @@ -159,12 +164,12 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff */ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { if (!lock.tryLock()) { - throw new IllegalStateException("This compressor is not thread-safe and is already in use"); + throw new IllegalStateException(IN_USE_ERROR); } try { if (statePtr == 0) { - throw new IllegalStateException("Compressor has been closed"); + throw new IllegalStateException(CLOSED_ERROR); } ByteBufferUtils.checkNotReadOnly(dest); ByteBufferUtils.checkRange(src, srcOff, srcLen); @@ -224,11 +229,16 @@ public boolean isClosed() { /** * Closes this compressor and releases native resources. - * After calling this method, all compress methods will throw {@link IllegalStateException} + * After calling this method, all compress methods will throw {@link IllegalStateException}. + * + * @throws IllegalStateException if the compressor is in use by another thread */ @Override public void close() { - lock.lock(); + if (!lock.tryLock()) { + throw new IllegalStateException(IN_USE_ERROR); + } + try { long ptr = statePtr; statePtr = 0; diff --git a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java index 4263c499..a6941a46 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java @@ -37,7 +37,8 @@ * ByteBuffer inputs that are neither array-backed nor direct fall back to the safe Java compressor. *

* Thread Safety: This class is NOT thread-safe. Each instance holds - * mutable native state and must be used by only one thread at a time. + * mutable native state and must be used by only one thread at a time. Concurrent use + * or close attempts fail fast with {@link IllegalStateException}. *

* Resource Management: This class holds native memory that must be freed. * Always use try-with-resources or explicitly call {@link #close()}. @@ -58,6 +59,10 @@ */ public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements AutoCloseable { + private static final String IN_USE_ERROR = "This compressor is not thread-safe and is already in use"; + private static final String CLOSED_ERROR = "Compressor has been closed"; + private static final String UNSUPPORTED_BUFFER_ERROR = "ByteBuffer must be direct or array-backed"; + private final ReentrantLock lock = new ReentrantLock(); private long statePtr; private final int compressionLevel; @@ -112,16 +117,16 @@ public int getCompressionLevel() { * @param maxDestLen the maximum number of bytes to write in dest * @return the compressed size * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalStateException if the compressor has been closed + * @throws IllegalStateException if the compressor has been closed or is already in use */ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { if (!lock.tryLock()) { - throw new IllegalStateException("This compressor is not thread-safe and is already in use"); + throw new IllegalStateException(IN_USE_ERROR); } try { if (statePtr == 0) { - throw new IllegalStateException("Compressor has been closed"); + throw new IllegalStateException(CLOSED_ERROR); } SafeUtils.checkRange(src, srcOff, srcLen); SafeUtils.checkRange(dest, destOff, maxDestLen); @@ -159,12 +164,12 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff */ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { if (!lock.tryLock()) { - throw new IllegalStateException("This compressor is not thread-safe and is already in use"); + throw new IllegalStateException(IN_USE_ERROR); } try { if (statePtr == 0) { - throw new IllegalStateException("Compressor has been closed"); + throw new IllegalStateException(CLOSED_ERROR); } ByteBufferUtils.checkNotReadOnly(dest); ByteBufferUtils.checkRange(src, srcOff, srcLen); @@ -224,11 +229,16 @@ public boolean isClosed() { /** * Closes this compressor and releases native resources. - * After calling this method, all compress methods will throw {@link IllegalStateException} + * After calling this method, all compress methods will throw {@link IllegalStateException}. + * + * @throws IllegalStateException if the compressor is in use by another thread */ @Override public void close() { - lock.lock(); + if (!lock.tryLock()) { + throw new IllegalStateException(IN_USE_ERROR); + } + try { long ptr = statePtr; statePtr = 0; From 8f168ec1fb0488de29dff0f9a6cbad675ac5a55f Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Thu, 9 Apr 2026 10:08:37 +0300 Subject: [PATCH 8/9] Throw on unsupported buffer attempt --- .../lz4/LZ4JNIFastResetCompressor.java | 26 +-- .../lz4/LZ4JNIHCFastResetCompressor.java | 26 +-- .../net/jpountz/lz4/LZ4FastResetTest.java | 181 +++++++++++++++--- 3 files changed, 174 insertions(+), 59 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java index 9be2dafa..9cb50551 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java @@ -34,7 +34,7 @@ *

* Each compression call is independent, making the output identical to {@link LZ4JNICompressor} * for the same acceleration level when operating on array-backed or direct buffers. - * ByteBuffer inputs that are neither array-backed nor direct fall back to the safe Java compressor. + * {@link ByteBuffer} inputs must be array-backed or direct. *

* Thread Safety: This class is NOT thread-safe. Each instance holds * mutable native state and must be used by only one thread at a time. Concurrent use @@ -66,7 +66,6 @@ public final class LZ4JNIFastResetCompressor extends LZ4Compressor implements Au private final ReentrantLock lock = new ReentrantLock(); private long statePtr; private final int acceleration; - private LZ4Compressor safeInstance; /** * Creates a new fast-reset compressor with default acceleration (1). @@ -148,8 +147,7 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff * Compresses {@code src[srcOff:srcOff+srcLen]} into * {@code dest[destOff:destOff+maxDestLen]}. *

- * Both buffers must be either direct or array-backed. If neither, the method - * falls back to the safe Java compressor. + * Both buffers must be either direct or array-backed. * {@link ByteBuffer} positions remain unchanged. * * @param src source data @@ -160,7 +158,8 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff * @param maxDestLen the maximum number of bytes to write in dest * @return the compressed size * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalStateException if the compressor has been closed + * @throws IllegalArgumentException if src or dest is neither array-backed nor direct + * @throws IllegalStateException if the compressor has been closed or is already in use */ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { if (!lock.tryLock()) { @@ -171,19 +170,12 @@ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int if (statePtr == 0) { throw new IllegalStateException(CLOSED_ERROR); } + checkByteBuffer(src); + checkByteBuffer(dest); ByteBufferUtils.checkNotReadOnly(dest); ByteBufferUtils.checkRange(src, srcOff, srcLen); ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { - LZ4Compressor safeCompressor = safeInstance; - if (safeCompressor == null) { - safeCompressor = LZ4Factory.safeInstance().fastCompressor(); - safeInstance = safeCompressor; - } - return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); - } - return compressNativeBuffers(statePtr, src, srcOff, srcLen, dest, destOff, maxDestLen); } finally { lock.unlock(); @@ -209,8 +201,10 @@ private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcL return result; } - private static boolean hasCompatibleBacking(ByteBuffer buffer) { - return buffer.hasArray() || buffer.isDirect(); + private static void checkByteBuffer(ByteBuffer buffer) { + if (!(buffer.hasArray() || buffer.isDirect())) { + throw new IllegalArgumentException(UNSUPPORTED_BUFFER_ERROR); + } } /** diff --git a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java index a6941a46..6356b895 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java @@ -34,7 +34,7 @@ *

* Each compression call is independent, making the output identical to {@link LZ4HCJNICompressor} * for the same compression level when operating on array-backed or direct buffers. - * ByteBuffer inputs that are neither array-backed nor direct fall back to the safe Java compressor. + * {@link ByteBuffer} inputs must be array-backed or direct. *

* Thread Safety: This class is NOT thread-safe. Each instance holds * mutable native state and must be used by only one thread at a time. Concurrent use @@ -66,7 +66,6 @@ public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements private final ReentrantLock lock = new ReentrantLock(); private long statePtr; private final int compressionLevel; - private LZ4Compressor safeInstance; /** * Creates a new HC fast-reset compressor with default compression level. @@ -148,8 +147,7 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff * Compresses {@code src[srcOff:srcOff+srcLen]} into * {@code dest[destOff:destOff+maxDestLen]}. *

- * Both buffers must be either direct or array-backed. If neither, the method - * falls back to the safe Java compressor. + * Both buffers must be either direct or array-backed. * {@link ByteBuffer} positions remain unchanged. * * @param src source data @@ -160,7 +158,8 @@ public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff * @param maxDestLen the maximum number of bytes to write in dest * @return the compressed size * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalStateException if the compressor has been closed + * @throws IllegalArgumentException if src or dest is neither array-backed nor direct + * @throws IllegalStateException if the compressor has been closed or is already in use */ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { if (!lock.tryLock()) { @@ -171,19 +170,12 @@ public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int if (statePtr == 0) { throw new IllegalStateException(CLOSED_ERROR); } + checkByteBuffer(src); + checkByteBuffer(dest); ByteBufferUtils.checkNotReadOnly(dest); ByteBufferUtils.checkRange(src, srcOff, srcLen); ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - if (!hasCompatibleBacking(src) || !hasCompatibleBacking(dest)) { - LZ4Compressor safeCompressor = safeInstance; - if (safeCompressor == null) { - safeCompressor = LZ4Factory.safeInstance().highCompressor(compressionLevel); - safeInstance = safeCompressor; - } - return safeCompressor.compress(src, srcOff, srcLen, dest, destOff, maxDestLen); - } - return compressNativeBuffers(statePtr, src, srcOff, srcLen, dest, destOff, maxDestLen); } finally { lock.unlock(); @@ -209,8 +201,10 @@ private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcL return result; } - private static boolean hasCompatibleBacking(ByteBuffer buffer) { - return buffer.hasArray() || buffer.isDirect(); + private static void checkByteBuffer(ByteBuffer buffer) { + if (!(buffer.hasArray() || buffer.isDirect())) { + throw new IllegalArgumentException(UNSUPPORTED_BUFFER_ERROR); + } } /** diff --git a/src/test/net/jpountz/lz4/LZ4FastResetTest.java b/src/test/net/jpountz/lz4/LZ4FastResetTest.java index f9aaa464..80a8c7a2 100644 --- a/src/test/net/jpountz/lz4/LZ4FastResetTest.java +++ b/src/test/net/jpountz/lz4/LZ4FastResetTest.java @@ -18,7 +18,6 @@ import junit.framework.TestCase; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import static org.junit.Assert.assertArrayEquals; @@ -114,21 +113,31 @@ public void testHighFastResetConstructorNormalization() { } } - public void testFastResetByteBufferFallbackUsesSafeJavaCompressor() throws Exception { + public void testFastResetByteBufferRoundTripWithDirectBuffers() { LZ4Factory factory = LZ4Factory.nativeInstance(); - ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); - int maxCompressedLength = LZ4Utils.maxCompressedLength(src.remaining()); + byte[] data = repeatedData(); + ByteBuffer src = directBuffer(data); + ByteBuffer compressed = ByteBuffer.allocateDirect(LZ4Utils.maxCompressedLength(data.length)); + ByteBuffer restored = ByteBuffer.allocateDirect(data.length); - try (LZ4JNIFastResetCompressor defaultCompressor = factory.fastResetCompressor(); - LZ4JNIFastResetCompressor acceleratedCompressor = factory.fastResetCompressor(9)) { - defaultCompressor.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); - acceleratedCompressor.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); + try (LZ4JNIFastResetCompressor compressor = factory.fastResetCompressor(9)) { + int compressedLength = compressor.compress(src, 0, src.remaining(), compressed, 0, compressed.capacity()); + int restoredLength = factory.safeDecompressor().decompress(compressed, 0, compressedLength, restored, 0, restored.capacity()); + + assertEquals(data.length, restoredLength); + assertArrayEquals(data, readBuffer(restored, restoredLength)); + } + } - Field safeInstanceField = LZ4JNIFastResetCompressor.class.getDeclaredField("safeInstance"); - safeInstanceField.setAccessible(true); + public void testFastResetByteBufferRejectsUnsupportedSourceBuffer() { + ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); + ByteBuffer dest = ByteBuffer.allocate(LZ4Utils.maxCompressedLength(src.remaining())); - assertSame(LZ4Factory.safeInstance().fastCompressor(), safeInstanceField.get(defaultCompressor)); - assertSame(LZ4Factory.safeInstance().fastCompressor(), safeInstanceField.get(acceleratedCompressor)); + try (LZ4JNIFastResetCompressor compressor = LZ4Factory.nativeInstance().fastResetCompressor()) { + compressor.compress(src, 0, src.remaining(), dest, 0, dest.remaining()); + fail(); + } catch (IllegalArgumentException expected) { + // expected } } @@ -147,21 +156,47 @@ public void testFastResetByteBufferAfterClose() { } } - public void testHighFastResetByteBufferFallbackKeepsCompressionLevel() throws Exception { + public void testFastResetRepeatedCompressCallsReuseState() { LZ4Factory factory = LZ4Factory.nativeInstance(); - ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); - int maxCompressedLength = LZ4Utils.maxCompressedLength(src.remaining()); - try (LZ4JNIHCFastResetCompressor level1 = factory.highFastResetCompressor(1); - LZ4JNIHCFastResetCompressor level17 = factory.highFastResetCompressor(17)) { - level1.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); - level17.compress(src.duplicate(), 0, src.remaining(), ByteBuffer.allocate(maxCompressedLength), 0, maxCompressedLength); + try (LZ4JNIFastResetCompressor compressor = factory.fastResetCompressor(9)) { + assertRepeatedCompressionReuse(compressor, factory.safeDecompressor()); + } + } + + public void testFastResetRepeatedCompressCallsRecoverAfterFailure() { + LZ4Factory factory = LZ4Factory.nativeInstance(); + + try (LZ4JNIFastResetCompressor compressor = factory.fastResetCompressor()) { + assertCompressionRecoversAfterFailure(compressor, factory.safeDecompressor()); + } + } + + public void testHighFastResetByteBufferRoundTripWithDirectBuffers() { + LZ4Factory factory = LZ4Factory.nativeInstance(); + byte[] data = repeatedData(); + ByteBuffer src = directBuffer(data); + ByteBuffer compressed = ByteBuffer.allocateDirect(LZ4Utils.maxCompressedLength(data.length)); + ByteBuffer restored = ByteBuffer.allocateDirect(data.length); - Field safeInstanceField = LZ4JNIHCFastResetCompressor.class.getDeclaredField("safeInstance"); - safeInstanceField.setAccessible(true); + try (LZ4JNIHCFastResetCompressor compressor = factory.highFastResetCompressor(9)) { + int compressedLength = compressor.compress(src, 0, src.remaining(), compressed, 0, compressed.capacity()); + int restoredLength = factory.safeDecompressor().decompress(compressed, 0, compressedLength, restored, 0, restored.capacity()); - assertSame(LZ4Factory.safeInstance().highCompressor(1), safeInstanceField.get(level1)); - assertSame(LZ4Factory.safeInstance().highCompressor(17), safeInstanceField.get(level17)); + assertEquals(data.length, restoredLength); + assertArrayEquals(data, readBuffer(restored, restoredLength)); + } + } + + public void testHighFastResetByteBufferRejectsUnsupportedSourceBuffer() { + ByteBuffer src = ByteBuffer.wrap(repeatedData()).asReadOnlyBuffer(); + ByteBuffer dest = ByteBuffer.allocate(LZ4Utils.maxCompressedLength(src.remaining())); + + try (LZ4JNIHCFastResetCompressor compressor = LZ4Factory.nativeInstance().highFastResetCompressor()) { + compressor.compress(src, 0, src.remaining(), dest, 0, dest.remaining()); + fail(); + } catch (IllegalArgumentException expected) { + // expected } } @@ -180,6 +215,22 @@ public void testHighFastResetByteBufferAfterClose() { } } + public void testHighFastResetRepeatedCompressCallsReuseState() { + LZ4Factory factory = LZ4Factory.nativeInstance(); + + try (LZ4JNIHCFastResetCompressor compressor = factory.highFastResetCompressor(9)) { + assertRepeatedCompressionReuse(compressor, factory.safeDecompressor()); + } + } + + public void testHighFastResetRepeatedCompressCallsRecoverAfterFailure() { + LZ4Factory factory = LZ4Factory.nativeInstance(); + + try (LZ4JNIHCFastResetCompressor compressor = factory.highFastResetCompressor()) { + assertCompressionRecoversAfterFailure(compressor, factory.safeDecompressor()); + } + } + public void testFastResetMethodsRequireNativeFactory() { assertFastResetUnsupported(LZ4Factory.safeInstance()); assertFastResetUnsupported(LZ4Factory.unsafeInstance()); @@ -194,16 +245,92 @@ private static byte[] repeatedData() { return data; } - private static void assertFastResetUnsupported(LZ4Factory factory) { + private static ByteBuffer directBuffer(byte[] data) { + ByteBuffer buffer = ByteBuffer.allocateDirect(data.length); + buffer.put(data); + buffer.flip(); + return buffer; + } + + private static byte[] alternateData() { + byte[] data = new byte[333]; + for (int i = 0; i < data.length; ++i) { + data[i] = (byte) ((i * 31) ^ 0x5A); + } + return data; + } + + private static byte[] readBuffer(ByteBuffer buffer, int length) { + ByteBuffer duplicate = buffer.duplicate(); + duplicate.position(0); + duplicate.limit(length); + byte[] bytes = new byte[length]; + duplicate.get(bytes); + return bytes; + } + + private static void assertRepeatedCompressionReuse(LZ4Compressor compressor, LZ4SafeDecompressor decompressor) { + byte[] first = repeatedData(); + byte[] second = alternateData(); + + assertArrayRoundTrip(compressor, decompressor, first); + assertArrayRoundTrip(compressor, decompressor, second); + assertDirectByteBufferRoundTrip(compressor, decompressor, second); + assertDirectByteBufferRoundTrip(compressor, decompressor, first); + } + + private static void assertCompressionRecoversAfterFailure(LZ4Compressor compressor, LZ4SafeDecompressor decompressor) { + byte[] data = repeatedData(); + byte[] tooSmallDest = new byte[1]; + try { - factory.fastResetCompressor(); + compressor.compress(data, 0, data.length, tooSmallDest, 0, tooSmallDest.length); + fail(); + } catch (LZ4Exception expected) { + // expected + } + + assertArrayRoundTrip(compressor, decompressor, alternateData()); + assertDirectByteBufferRoundTrip(compressor, decompressor, data); + } + + private static void assertArrayRoundTrip(LZ4Compressor compressor, LZ4SafeDecompressor decompressor, byte[] data) { + byte[] compressed = new byte[compressor.maxCompressedLength(data.length)]; + int compressedLength = compressor.compress(data, 0, data.length, compressed, 0, compressed.length); + byte[] restored = new byte[data.length]; + int restoredLength = decompressor.decompress(compressed, 0, compressedLength, restored, 0); + + assertEquals(data.length, restoredLength); + assertArrayEquals(data, restored); + } + + private static void assertDirectByteBufferRoundTrip(LZ4Compressor compressor, LZ4SafeDecompressor decompressor, byte[] data) { + ByteBuffer src = directBuffer(data); + ByteBuffer compressed = ByteBuffer.allocateDirect(compressor.maxCompressedLength(data.length)); + ByteBuffer restored = ByteBuffer.allocateDirect(data.length); + + int srcPosition = src.position(); + int compressedPosition = compressed.position(); + int restoredPosition = restored.position(); + + int compressedLength = compressor.compress(src, 0, src.remaining(), compressed, 0, compressed.capacity()); + int restoredLength = decompressor.decompress(compressed, 0, compressedLength, restored, 0, restored.capacity()); + + assertEquals(srcPosition, src.position()); + assertEquals(compressedPosition, compressed.position()); + assertEquals(restoredPosition, restored.position()); + assertEquals(data.length, restoredLength); + assertArrayEquals(data, readBuffer(restored, restoredLength)); + } + + private static void assertFastResetUnsupported(LZ4Factory factory) { + try (LZ4JNIFastResetCompressor ignored = factory.fastResetCompressor()) { fail(); } catch (UnsupportedOperationException expected) { // expected } - try { - factory.highFastResetCompressor(); + try (LZ4JNIHCFastResetCompressor ignored = factory.highFastResetCompressor()) { fail(); } catch (UnsupportedOperationException expected) { // expected From b50154615cdecd66a34058a9184748e90062dea6 Mon Sep 17 00:00:00 2001 From: Artsiom Chmutau Date: Thu, 9 Apr 2026 10:43:07 +0300 Subject: [PATCH 9/9] Move shared logic to an abstract base class --- .../AbstractLZ4JNIFastResetCompressor.java | 182 ++++++++++++++++++ .../lz4/LZ4JNIFastResetCompressor.java | 161 +--------------- .../lz4/LZ4JNIHCFastResetCompressor.java | 161 +--------------- 3 files changed, 202 insertions(+), 302 deletions(-) create mode 100644 src/java/net/jpountz/lz4/AbstractLZ4JNIFastResetCompressor.java diff --git a/src/java/net/jpountz/lz4/AbstractLZ4JNIFastResetCompressor.java b/src/java/net/jpountz/lz4/AbstractLZ4JNIFastResetCompressor.java new file mode 100644 index 00000000..e57ddc47 --- /dev/null +++ b/src/java/net/jpountz/lz4/AbstractLZ4JNIFastResetCompressor.java @@ -0,0 +1,182 @@ +package net.jpountz.lz4; + +/* + * Copyright 2020 Adrien Grand and the lz4-java contributors. + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import net.jpountz.util.ByteBufferUtils; +import net.jpountz.util.SafeUtils; + +import java.nio.ByteBuffer; +import java.util.concurrent.locks.ReentrantLock; + +abstract class AbstractLZ4JNIFastResetCompressor extends LZ4Compressor implements AutoCloseable { + + private static final String IN_USE_ERROR = "This compressor is not thread-safe and is already in use"; + private static final String CLOSED_ERROR = "Compressor has been closed"; + private static final String UNSUPPORTED_BUFFER_ERROR = "ByteBuffer must be direct or array-backed"; + + private final ReentrantLock lock = new ReentrantLock(); + private long statePtr; + + AbstractLZ4JNIFastResetCompressor(long statePtr, String allocationFailureMessage) { + if (statePtr == 0) { + throw new LZ4Exception(allocationFailureMessage); + } + this.statePtr = statePtr; + } + + /** + * Compresses {@code src[srcOff:srcOff+srcLen]} into + * {@code dest[destOff:destOff+maxDestLen]}. + * + * @param src source data + * @param srcOff the start offset in src + * @param srcLen the number of bytes to compress + * @param dest destination buffer + * @param destOff the start offset in dest + * @param maxDestLen the maximum number of bytes to write in dest + * @return the compressed size + * @throws LZ4Exception if maxDestLen is too small + * @throws IllegalStateException if the compressor has been closed or is already in use + */ + @Override + public final int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { + if (!lock.tryLock()) { + throw new IllegalStateException(IN_USE_ERROR); + } + + try { + long ptr = checkOpen(); + SafeUtils.checkRange(src, srcOff, srcLen); + SafeUtils.checkRange(dest, destOff, maxDestLen); + + final int result = compressNative( + ptr, src, null, srcOff, srcLen, + dest, null, destOff, maxDestLen); + return checkResult(result); + } finally { + lock.unlock(); + } + } + + /** + * Compresses {@code src[srcOff:srcOff+srcLen]} into + * {@code dest[destOff:destOff+maxDestLen]}. + *

+ * Both buffers must be either direct or array-backed. + * {@link ByteBuffer} positions remain unchanged. + * + * @param src source data + * @param srcOff the start offset in src + * @param srcLen the number of bytes to compress + * @param dest destination buffer + * @param destOff the start offset in dest + * @param maxDestLen the maximum number of bytes to write in dest + * @return the compressed size + * @throws LZ4Exception if maxDestLen is too small + * @throws IllegalArgumentException if src or dest is neither array-backed nor direct + * @throws IllegalStateException if the compressor has been closed or is already in use + */ + @Override + public final int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { + if (!lock.tryLock()) { + throw new IllegalStateException(IN_USE_ERROR); + } + + try { + long ptr = checkOpen(); + checkByteBuffer(src); + checkByteBuffer(dest); + ByteBufferUtils.checkNotReadOnly(dest); + ByteBufferUtils.checkRange(src, srcOff, srcLen); + ByteBufferUtils.checkRange(dest, destOff, maxDestLen); + + byte[] srcArr = src.hasArray() ? src.array() : null; + byte[] destArr = dest.hasArray() ? dest.array() : null; + ByteBuffer srcBuf = srcArr == null ? src : null; + ByteBuffer destBuf = destArr == null ? dest : null; + int srcBufferOff = srcOff + (srcArr != null ? src.arrayOffset() : 0); + int destBufferOff = destOff + (destArr != null ? dest.arrayOffset() : 0); + + final int result = compressNative( + ptr, srcArr, srcBuf, srcBufferOff, srcLen, + destArr, destBuf, destBufferOff, maxDestLen); + return checkResult(result); + } finally { + lock.unlock(); + } + } + + public final boolean isClosed() { + lock.lock(); + try { + return statePtr == 0; + } finally { + lock.unlock(); + } + } + + /** + * Closes this compressor and releases native resources. + * After calling this method, all compress methods will throw {@link IllegalStateException}. + * + * @throws IllegalStateException if the compressor is in use by another thread + */ + @Override + public final void close() { + if (!lock.tryLock()) { + throw new IllegalStateException(IN_USE_ERROR); + } + + try { + long ptr = statePtr; + statePtr = 0; + if (ptr != 0) { + freeState(ptr); + } + } finally { + lock.unlock(); + } + } + + private long checkOpen() { + if (statePtr == 0) { + throw new IllegalStateException(CLOSED_ERROR); + } + return statePtr; + } + + private static int checkResult(int result) { + if (result <= 0) { + throw new LZ4Exception("maxDestLen is too small"); + } + return result; + } + + private static void checkByteBuffer(ByteBuffer buffer) { + if (!(buffer.hasArray() || buffer.isDirect())) { + throw new IllegalArgumentException(UNSUPPORTED_BUFFER_ERROR); + } + } + + protected abstract int compressNative(long ptr, byte[] srcArr, ByteBuffer srcBuf, int srcOff, int srcLen, + byte[] destArr, ByteBuffer destBuf, int destOff, int maxDestLen); + + protected abstract void freeState(long ptr); +} + + + diff --git a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java index 9cb50551..d70251d0 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIFastResetCompressor.java @@ -16,11 +16,7 @@ * limitations under the License. */ -import net.jpountz.util.ByteBufferUtils; -import net.jpountz.util.SafeUtils; - import java.nio.ByteBuffer; -import java.util.concurrent.locks.ReentrantLock; import static net.jpountz.lz4.LZ4Constants.MIN_ACCELERATION; import static net.jpountz.lz4.LZ4Constants.MAX_ACCELERATION; @@ -57,14 +53,8 @@ * @see LZ4Factory#fastResetCompressor(int) * @see LZ4JNICompressor */ -public final class LZ4JNIFastResetCompressor extends LZ4Compressor implements AutoCloseable { - - private static final String IN_USE_ERROR = "This compressor is not thread-safe and is already in use"; - private static final String CLOSED_ERROR = "Compressor has been closed"; - private static final String UNSUPPORTED_BUFFER_ERROR = "ByteBuffer must be direct or array-backed"; +public final class LZ4JNIFastResetCompressor extends AbstractLZ4JNIFastResetCompressor { - private final ReentrantLock lock = new ReentrantLock(); - private long statePtr; private final int acceleration; /** @@ -82,17 +72,13 @@ public final class LZ4JNIFastResetCompressor extends LZ4Compressor implements Au * @param acceleration acceleration factor (1 = default, higher = faster but less compression) */ LZ4JNIFastResetCompressor(int acceleration) { + super(LZ4JNI.LZ4_createStream(), "Failed to allocate LZ4 state"); if (acceleration < MIN_ACCELERATION) { acceleration = MIN_ACCELERATION; } else if (acceleration > MAX_ACCELERATION) { acceleration = MAX_ACCELERATION; } this.acceleration = acceleration; - long ptr = LZ4JNI.LZ4_createStream(); - if (ptr == 0) { - throw new LZ4Exception("Failed to allocate LZ4 state"); - } - this.statePtr = ptr; } /** @@ -104,144 +90,17 @@ public int getAcceleration() { return acceleration; } - /** - * Compresses {@code src[srcOff:srcOff+srcLen]} into - * {@code dest[destOff:destOff+maxDestLen]}. - * - * @param src source data - * @param srcOff the start offset in src - * @param srcLen the number of bytes to compress - * @param dest destination buffer - * @param destOff the start offset in dest - * @param maxDestLen the maximum number of bytes to write in dest - * @return the compressed size - * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalStateException if the compressor has been closed or is already in use - */ - public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { - if (!lock.tryLock()) { - throw new IllegalStateException(IN_USE_ERROR); - } - - try { - if (statePtr == 0) { - throw new IllegalStateException(CLOSED_ERROR); - } - SafeUtils.checkRange(src, srcOff, srcLen); - SafeUtils.checkRange(dest, destOff, maxDestLen); - - final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( - statePtr, src, null, srcOff, srcLen, - dest, null, destOff, maxDestLen, acceleration); - - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); - } - return result; - } finally { - lock.unlock(); - } - } - - /** - * Compresses {@code src[srcOff:srcOff+srcLen]} into - * {@code dest[destOff:destOff+maxDestLen]}. - *

- * Both buffers must be either direct or array-backed. - * {@link ByteBuffer} positions remain unchanged. - * - * @param src source data - * @param srcOff the start offset in src - * @param srcLen the number of bytes to compress - * @param dest destination buffer - * @param destOff the start offset in dest - * @param maxDestLen the maximum number of bytes to write in dest - * @return the compressed size - * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalArgumentException if src or dest is neither array-backed nor direct - * @throws IllegalStateException if the compressor has been closed or is already in use - */ - public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { - if (!lock.tryLock()) { - throw new IllegalStateException(IN_USE_ERROR); - } - - try { - if (statePtr == 0) { - throw new IllegalStateException(CLOSED_ERROR); - } - checkByteBuffer(src); - checkByteBuffer(dest); - ByteBufferUtils.checkNotReadOnly(dest); - ByteBufferUtils.checkRange(src, srcOff, srcLen); - ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - - return compressNativeBuffers(statePtr, src, srcOff, srcLen, dest, destOff, maxDestLen); - } finally { - lock.unlock(); - } - } - - private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcLen, - ByteBuffer dest, int destOff, int maxDestLen) { - byte[] srcArr = src.hasArray() ? src.array() : null; - byte[] destArr = dest.hasArray() ? dest.array() : null; - ByteBuffer srcBuf = srcArr == null ? src : null; - ByteBuffer destBuf = destArr == null ? dest : null; - int srcBufferOff = srcOff + (srcArr != null ? src.arrayOffset() : 0); - int destBufferOff = destOff + (destArr != null ? dest.arrayOffset() : 0); - - final int result = LZ4JNI.LZ4_compress_fast_extState_fastReset( - ptr, srcArr, srcBuf, srcBufferOff, srcLen, - destArr, destBuf, destBufferOff, maxDestLen, acceleration); - - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); - } - return result; - } - - private static void checkByteBuffer(ByteBuffer buffer) { - if (!(buffer.hasArray() || buffer.isDirect())) { - throw new IllegalArgumentException(UNSUPPORTED_BUFFER_ERROR); - } - } - - /** - * Returns true if this compressor has been closed. - * - * @return true if closed - */ - public boolean isClosed() { - lock.lock(); - try { - return statePtr == 0; - } finally { - lock.unlock(); - } + @Override + protected int compressNative(long ptr, byte[] srcArr, ByteBuffer srcBuf, int srcOff, int srcLen, + byte[] destArr, ByteBuffer destBuf, int destOff, int maxDestLen) { + return LZ4JNI.LZ4_compress_fast_extState_fastReset( + ptr, srcArr, srcBuf, srcOff, srcLen, + destArr, destBuf, destOff, maxDestLen, acceleration); } - /** - * Closes this compressor and releases native resources. - * After calling this method, all compress methods will throw {@link IllegalStateException}. - * - * @throws IllegalStateException if the compressor is in use by another thread - */ @Override - public void close() { - if (!lock.tryLock()) { - throw new IllegalStateException(IN_USE_ERROR); - } - - try { - long ptr = statePtr; - statePtr = 0; - if (ptr != 0) { - LZ4JNI.LZ4_freeStream(ptr); - } - } finally { - lock.unlock(); - } + protected void freeState(long ptr) { + LZ4JNI.LZ4_freeStream(ptr); } @Override diff --git a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java index 6356b895..b6c2cb34 100644 --- a/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java +++ b/src/java/net/jpountz/lz4/LZ4JNIHCFastResetCompressor.java @@ -19,11 +19,7 @@ import static net.jpountz.lz4.LZ4Constants.DEFAULT_COMPRESSION_LEVEL; import static net.jpountz.lz4.LZ4Constants.MAX_COMPRESSION_LEVEL; -import net.jpountz.util.ByteBufferUtils; -import net.jpountz.util.SafeUtils; - import java.nio.ByteBuffer; -import java.util.concurrent.locks.ReentrantLock; /** * An optimized LZ4 HC compressor that uses native {@code LZ4_compress_HC_extStateHC_fastReset}. @@ -57,14 +53,8 @@ * @see LZ4Factory#highFastResetCompressor(int) * @see LZ4HCJNICompressor */ -public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements AutoCloseable { - - private static final String IN_USE_ERROR = "This compressor is not thread-safe and is already in use"; - private static final String CLOSED_ERROR = "Compressor has been closed"; - private static final String UNSUPPORTED_BUFFER_ERROR = "ByteBuffer must be direct or array-backed"; +public final class LZ4JNIHCFastResetCompressor extends AbstractLZ4JNIFastResetCompressor { - private final ReentrantLock lock = new ReentrantLock(); - private long statePtr; private final int compressionLevel; /** @@ -82,17 +72,13 @@ public final class LZ4JNIHCFastResetCompressor extends LZ4Compressor implements * @param compressionLevel compression level (1-17, higher = better compression) */ LZ4JNIHCFastResetCompressor(int compressionLevel) { + super(LZ4JNI.LZ4_createStreamHC(), "Failed to allocate LZ4 HC state"); if (compressionLevel > MAX_COMPRESSION_LEVEL) { compressionLevel = MAX_COMPRESSION_LEVEL; } else if (compressionLevel < 1) { compressionLevel = DEFAULT_COMPRESSION_LEVEL; } this.compressionLevel = compressionLevel; - long ptr = LZ4JNI.LZ4_createStreamHC(); - if (ptr == 0) { - throw new LZ4Exception("Failed to allocate LZ4 HC state"); - } - this.statePtr = ptr; } /** @@ -104,144 +90,17 @@ public int getCompressionLevel() { return compressionLevel; } - /** - * Compresses {@code src[srcOff:srcOff+srcLen]} into - * {@code dest[destOff:destOff+maxDestLen]}. - * - * @param src source data - * @param srcOff the start offset in src - * @param srcLen the number of bytes to compress - * @param dest destination buffer - * @param destOff the start offset in dest - * @param maxDestLen the maximum number of bytes to write in dest - * @return the compressed size - * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalStateException if the compressor has been closed or is already in use - */ - public int compress(byte[] src, int srcOff, int srcLen, byte[] dest, int destOff, int maxDestLen) { - if (!lock.tryLock()) { - throw new IllegalStateException(IN_USE_ERROR); - } - - try { - if (statePtr == 0) { - throw new IllegalStateException(CLOSED_ERROR); - } - SafeUtils.checkRange(src, srcOff, srcLen); - SafeUtils.checkRange(dest, destOff, maxDestLen); - - final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( - statePtr, src, null, srcOff, srcLen, - dest, null, destOff, maxDestLen, compressionLevel); - - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); - } - return result; - } finally { - lock.unlock(); - } - } - - /** - * Compresses {@code src[srcOff:srcOff+srcLen]} into - * {@code dest[destOff:destOff+maxDestLen]}. - *

- * Both buffers must be either direct or array-backed. - * {@link ByteBuffer} positions remain unchanged. - * - * @param src source data - * @param srcOff the start offset in src - * @param srcLen the number of bytes to compress - * @param dest destination buffer - * @param destOff the start offset in dest - * @param maxDestLen the maximum number of bytes to write in dest - * @return the compressed size - * @throws LZ4Exception if maxDestLen is too small - * @throws IllegalArgumentException if src or dest is neither array-backed nor direct - * @throws IllegalStateException if the compressor has been closed or is already in use - */ - public int compress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, int destOff, int maxDestLen) { - if (!lock.tryLock()) { - throw new IllegalStateException(IN_USE_ERROR); - } - - try { - if (statePtr == 0) { - throw new IllegalStateException(CLOSED_ERROR); - } - checkByteBuffer(src); - checkByteBuffer(dest); - ByteBufferUtils.checkNotReadOnly(dest); - ByteBufferUtils.checkRange(src, srcOff, srcLen); - ByteBufferUtils.checkRange(dest, destOff, maxDestLen); - - return compressNativeBuffers(statePtr, src, srcOff, srcLen, dest, destOff, maxDestLen); - } finally { - lock.unlock(); - } - } - - private int compressNativeBuffers(long ptr, ByteBuffer src, int srcOff, int srcLen, - ByteBuffer dest, int destOff, int maxDestLen) { - byte[] srcArr = src.hasArray() ? src.array() : null; - byte[] destArr = dest.hasArray() ? dest.array() : null; - ByteBuffer srcBuf = srcArr == null ? src : null; - ByteBuffer destBuf = destArr == null ? dest : null; - int srcBufferOff = srcOff + (srcArr != null ? src.arrayOffset() : 0); - int destBufferOff = destOff + (destArr != null ? dest.arrayOffset() : 0); - - final int result = LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( - ptr, srcArr, srcBuf, srcBufferOff, srcLen, - destArr, destBuf, destBufferOff, maxDestLen, compressionLevel); - - if (result <= 0) { - throw new LZ4Exception("maxDestLen is too small"); - } - return result; - } - - private static void checkByteBuffer(ByteBuffer buffer) { - if (!(buffer.hasArray() || buffer.isDirect())) { - throw new IllegalArgumentException(UNSUPPORTED_BUFFER_ERROR); - } - } - - /** - * Returns true if this compressor has been closed. - * - * @return true if closed - */ - public boolean isClosed() { - lock.lock(); - try { - return statePtr == 0; - } finally { - lock.unlock(); - } + @Override + protected int compressNative(long ptr, byte[] srcArr, ByteBuffer srcBuf, int srcOff, int srcLen, + byte[] destArr, ByteBuffer destBuf, int destOff, int maxDestLen) { + return LZ4JNI.LZ4_compress_HC_extStateHC_fastReset( + ptr, srcArr, srcBuf, srcOff, srcLen, + destArr, destBuf, destOff, maxDestLen, compressionLevel); } - /** - * Closes this compressor and releases native resources. - * After calling this method, all compress methods will throw {@link IllegalStateException}. - * - * @throws IllegalStateException if the compressor is in use by another thread - */ @Override - public void close() { - if (!lock.tryLock()) { - throw new IllegalStateException(IN_USE_ERROR); - } - - try { - long ptr = statePtr; - statePtr = 0; - if (ptr != 0) { - LZ4JNI.LZ4_freeStreamHC(ptr); - } - } finally { - lock.unlock(); - } + protected void freeState(long ptr) { + LZ4JNI.LZ4_freeStreamHC(ptr); } @Override