diff --git a/zstd/src/main/java/io/github/dfa1/zstd/NativeCall.java b/zstd/src/main/java/io/github/dfa1/zstd/NativeCall.java index 463e0a9..88c9d06 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/NativeCall.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/NativeCall.java @@ -2,6 +2,7 @@ import java.lang.foreign.MemorySegment; import java.nio.charset.StandardCharsets; +import java.util.Objects; /// Package-private helpers that adapt raw FFM downcalls to the zstd error /// convention: run a native call, decode a zstd `size_t` error code into a @@ -60,6 +61,7 @@ private static String errorName(long code) { /// by native (off-heap) memory, since its address is dereferenced in C. Fails /// fast with a clear message instead of the FFM linker's cryptic error. static void requireNative(MemorySegment seg, String name) { + Objects.requireNonNull(seg, name); if (!seg.isNative()) { throw new IllegalArgumentException( name + " must be a native (off-heap) MemorySegment; got a heap segment"); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java b/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java index 5385de7..4a4b8c6 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java @@ -3,6 +3,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.nio.charset.StandardCharsets; +import java.util.Objects; import static java.lang.foreign.ValueLayout.JAVA_BYTE; @@ -19,9 +20,9 @@ public final class Zstd { /// Sentinel returned by zstd when a frame carries no decompressed-size header. - private static final long CONTENTSIZE_UNKNOWN = -1L; + static final long CONTENTSIZE_UNKNOWN = -1L; /// Sentinel returned by zstd when the input is not a valid zstd frame. - private static final long CONTENTSIZE_ERROR = -2L; + static final long CONTENTSIZE_ERROR = -2L; /// Compresses `src` at the library default level. /// @@ -38,6 +39,7 @@ public static byte[] compress(byte[] src) { /// higher is smaller but slower /// @return a self-describing zstd frame public static byte[] compress(byte[] src, int level) { + Objects.requireNonNull(src, "src"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = copyIn(arena, src); long bound = compressBound(src.length); @@ -56,6 +58,7 @@ public static byte[] compress(byte[] src, int level) { /// @throws ZstdException if the frame is invalid or its content size is not stored; /// use {@link #decompress(byte[], int)} for the latter public static byte[] decompress(byte[] compressed) { + Objects.requireNonNull(compressed, "compressed"); long size = frameContentSize(compressed); if (size == CONTENTSIZE_UNKNOWN) { throw new ZstdException("decompressed size not stored in frame; call decompress(src, maxSize)"); @@ -74,6 +77,7 @@ public static byte[] decompress(byte[] compressed) { /// @return the original bytes (length ≤ `maxSize`) /// @throws ZstdException if the frame is invalid or larger than `maxSize` public static byte[] decompress(byte[] compressed, int maxSize) { + Objects.requireNonNull(compressed, "compressed"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = copyIn(arena, compressed); MemorySegment out = arena.allocate(Math.max(maxSize, 1)); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressCtx.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressCtx.java index ded4987..50e40fb 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressCtx.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressCtx.java @@ -2,6 +2,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.util.Objects; /// A reusable compression context. /// @@ -95,6 +96,7 @@ public ZstdCompressCtx windowLog(int windowLog) { /// @param src the bytes to compress /// @return a self-describing zstd frame public byte[] compress(byte[] src) { + Objects.requireNonNull(src, "src"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = Zstd.copyIn(arena, src); long bound = Zstd.compressBound(src.length); @@ -119,6 +121,8 @@ private void setParam(ZstdCompressParameter parameter, int value) { /// @param dict the dictionary to compress against /// @return a self-describing zstd frame public byte[] compress(byte[] src, ZstdDictionary dict) { + Objects.requireNonNull(src, "src"); + Objects.requireNonNull(dict, "dict"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = Zstd.copyIn(arena, src); byte[] d = dict.raw(); @@ -138,6 +142,8 @@ public byte[] compress(byte[] src, ZstdDictionary dict) { /// @param dict the pre-digested compression dictionary /// @return a self-describing zstd frame public byte[] compress(byte[] src, ZstdCompressDict dict) { + Objects.requireNonNull(src, "src"); + Objects.requireNonNull(dict, "dict"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = Zstd.copyIn(arena, src); long bound = Zstd.compressBound(src.length); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDict.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDict.java index c900a41..6048635 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDict.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDict.java @@ -2,6 +2,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.util.Objects; /// A dictionary digested once for compression at a fixed level. /// @@ -10,6 +11,8 @@ /// cost — the right choice when compressing many payloads against the same /// dictionary. The raw {@link ZstdDictionary} bytes are copied into native /// memory, so the source may be discarded afterwards. +/// +/// Immutable once built and safe to share across threads (the digested dictionary is read-only). public final class ZstdCompressDict extends NativeObject { private final int level; @@ -54,6 +57,7 @@ public ZstdCompressDict(MemorySegment dict) { } private static MemorySegment create(ZstdDictionary dict, int level) { + Objects.requireNonNull(dict, "dict"); try (Arena arena = Arena.ofConfined()) { byte[] raw = dict.raw(); MemorySegment d = Zstd.copyIn(arena, raw); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java index 3676d74..7e85955 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java @@ -26,6 +26,8 @@ /// } while (!r.isComplete()); /// } /// } +/// +/// Not thread-safe: confine an instance to a single thread. public final class ZstdCompressStream extends NativeObject { private final Arena arena = Arena.ofConfined(); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressCtx.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressCtx.java index d709916..bfa42d1 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressCtx.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressCtx.java @@ -2,6 +2,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.util.Objects; /// A reusable decompression context. /// @@ -52,6 +53,7 @@ public ZstdDecompressCtx windowLogMax(int windowLogMax) { /// @param maxSize upper bound on the decompressed length /// @return the original bytes public byte[] decompress(byte[] compressed, int maxSize) { + Objects.requireNonNull(compressed, "compressed"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = Zstd.copyIn(arena, compressed); MemorySegment out = arena.allocate(Math.max(maxSize, 1)); @@ -72,6 +74,8 @@ public byte[] decompress(byte[] compressed, int maxSize) { /// @param dict the dictionary the frame was compressed against /// @return the original bytes public byte[] decompress(byte[] compressed, int maxSize, ZstdDictionary dict) { + Objects.requireNonNull(compressed, "compressed"); + Objects.requireNonNull(dict, "dict"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = Zstd.copyIn(arena, compressed); byte[] d = dict.raw(); @@ -90,6 +94,8 @@ public byte[] decompress(byte[] compressed, int maxSize, ZstdDictionary dict) { /// @param dict the pre-digested decompression dictionary /// @return the original bytes public byte[] decompress(byte[] compressed, int maxSize, ZstdDecompressDict dict) { + Objects.requireNonNull(compressed, "compressed"); + Objects.requireNonNull(dict, "dict"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = Zstd.copyIn(arena, compressed); MemorySegment out = arena.allocate(Math.max(maxSize, 1)); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressDict.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressDict.java index aefe4ca..d695cc0 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressDict.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressDict.java @@ -2,6 +2,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.util.Objects; /// A dictionary digested once for decompression. /// @@ -9,6 +10,8 @@ /// {@link ZstdDecompressCtx#decompress(byte[], int, ZstdDecompressDict)} call /// skips that cost. The raw {@link ZstdDictionary} bytes are copied into native /// memory, so the source may be discarded afterwards. +/// +/// Immutable once built and safe to share across threads (the digested dictionary is read-only). public final class ZstdDecompressDict extends NativeObject { /// Digests `dict` for decompression. @@ -31,6 +34,7 @@ public ZstdDecompressDict(MemorySegment dict) { } private static MemorySegment create(ZstdDictionary dict) { + Objects.requireNonNull(dict, "dict"); try (Arena arena = Arena.ofConfined()) { byte[] raw = dict.raw(); MemorySegment d = Zstd.copyIn(arena, raw); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressStream.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressStream.java index e0bedd7..db16a87 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressStream.java @@ -10,6 +10,8 @@ /// pipelines prefer [ZstdInputStream]. Feed compressed input, drain the /// destination when it fills; a result is [ZstdStreamResult#isComplete()] when /// the current frame is fully decoded. +/// +/// Not thread-safe: confine an instance to a single thread. public final class ZstdDecompressStream extends NativeObject { private final Arena arena = Arena.ofConfined(); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java index 876b5e8..1eae3c5 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java @@ -7,6 +7,7 @@ import java.lang.invoke.MethodHandle; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Objects; import static java.lang.foreign.ValueLayout.JAVA_BYTE; import static java.lang.foreign.ValueLayout.JAVA_DOUBLE; @@ -79,6 +80,7 @@ private ZstdDictionary(byte[] bytes) { /// @param raw dictionary bytes; defensively copied /// @return a dictionary wrapping a copy of `raw` public static ZstdDictionary of(byte[] raw) { + Objects.requireNonNull(raw, "raw"); return new ZstdDictionary(raw.clone()); } @@ -92,6 +94,7 @@ public static ZstdDictionary of(byte[] raw) { /// @return the trained dictionary /// @throws ZstdException if training fails (commonly: not enough sample data) public static ZstdDictionary train(List samples, int maxDictBytes) { + Objects.requireNonNull(samples, "samples"); if (samples.isEmpty()) { throw new ZstdException("cannot train a dictionary from zero samples"); } @@ -175,6 +178,7 @@ public static ZstdDictionary trainFastCover(List samples, int maxDictByt private static ZstdDictionary optimize(List samples, int maxDictBytes, int compressionLevel, boolean fast) { + Objects.requireNonNull(samples, "samples"); if (samples.isEmpty()) { throw new ZstdException("cannot train a dictionary from zero samples"); } @@ -228,6 +232,8 @@ private static ZstdDictionary optimize(List samples, int maxDictBytes, /// @throws ZstdException if finalisation fails public static ZstdDictionary finalizeFrom(byte[] content, List samples, int maxDictBytes, int compressionLevel) { + Objects.requireNonNull(content, "content"); + Objects.requireNonNull(samples, "samples"); if (samples.isEmpty()) { throw new ZstdException("cannot finalise a dictionary from zero samples"); } diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrame.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrame.java index 6619135..713f5c5 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrame.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrame.java @@ -13,9 +13,6 @@ /// data already off-heap (e.g. an mmap slice); see `docs/zero-copy.md`. public final class ZstdFrame { - /// Sentinel returned by `ZSTD_decompressBound` when the input is not valid. - private static final long CONTENT_SIZE_ERROR = -2L; - /// Tests whether `data` begins with a valid zstd frame (standard or skippable). /// /// @param data the bytes to inspect @@ -213,7 +210,7 @@ private static long decompressedBound(MemorySegment data, long size) { } catch (Throwable t) { throw NativeCall.rethrow(t); } - if (bound == CONTENT_SIZE_ERROR) { + if (bound == Zstd.CONTENTSIZE_ERROR) { throw new ZstdException("not valid zstd data"); } return bound; diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrameHeader.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrameHeader.java index 9be1a29..ac5e6ad 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrameHeader.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdFrameHeader.java @@ -22,14 +22,11 @@ public record ZstdFrameHeader( int dictId, boolean hasChecksum) { - /// Sentinel meaning the decompressed size is not recorded in the frame. - private static final long CONTENTSIZE_UNKNOWN = -1L; - /// The decompressed size, if the frame records it. /// /// @return the content size, or empty if the frame does not store it public OptionalLong contentSize() { - return frameContentSize == CONTENTSIZE_UNKNOWN + return frameContentSize == Zstd.CONTENTSIZE_UNKNOWN ? OptionalLong.empty() : OptionalLong.of(frameContentSize); } diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdInputStream.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdInputStream.java index de63fd2..223b8a0 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdInputStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdInputStream.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.util.Objects; import static java.lang.foreign.ValueLayout.JAVA_BYTE; @@ -22,6 +23,8 @@ /// zin.transferTo(sink); /// } /// } +/// +/// Not thread-safe: confine an instance to a single thread. public final class ZstdInputStream extends InputStream { private final InputStream in; @@ -55,7 +58,7 @@ public ZstdInputStream(InputStream in) { /// @param in the stream to read the compressed frame from /// @param dictionary the dictionary the frame was compressed against, or `null` for none public ZstdInputStream(InputStream in, ZstdDictionary dictionary) { - this.in = in; + this.in = Objects.requireNonNull(in, "in"); MemorySegment d = null; try { d = (MemorySegment) Bindings.CREATE_DCTX.invokeExact(); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java index f1e03f0..d8d2d76 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java @@ -4,6 +4,7 @@ import java.io.OutputStream; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.util.Objects; import static java.lang.foreign.ValueLayout.JAVA_BYTE; @@ -22,6 +23,8 @@ /// source.transferTo(zout); /// } /// } +/// +/// Not thread-safe: confine an instance to a single thread. public final class ZstdOutputStream extends OutputStream { // ZSTD_cParameter / ZSTD_EndDirective values from zstd.h — see @@ -92,7 +95,7 @@ private void setPledgedSrcSize(long pledgedSrcSize) { /// @param level the compression level /// @param dictionary the dictionary to compress against, or `null` for none public ZstdOutputStream(OutputStream out, int level, ZstdDictionary dictionary) { - this.out = out; + this.out = Objects.requireNonNull(out, "out"); MemorySegment c = null; try { c = (MemorySegment) Bindings.CREATE_CCTX.invokeExact(); diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java index d46e27f..c71d0d1 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java @@ -125,6 +125,21 @@ void rejectsOversizedFrameForBuffer() { // Then it fails assertThatThrownBy(result).isInstanceOf(ZstdException.class); } + + @Test + void rejectsNullInputWithANamedMessage() { + // When null is passed where bytes are required + ThrowingCallable compressNull = () -> Zstd.compress(null); + ThrowingCallable decompressNull = () -> Zstd.decompress(null); + + // Then it fails fast with a NullPointerException naming the parameter + assertThatThrownBy(compressNull) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("src"); + assertThatThrownBy(decompressNull) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("compressed"); + } } @Nested