From 08bca758de60ec5d33a95fc7b457a8a3eaa26235 Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Fri, 26 Jun 2026 09:27:07 +0200 Subject: [PATCH 1/4] build: Maven Central deployment setup Mirror the vortex-java publish flow: - root pom: scm, plugin-version properties, and a `release` profile (source + javadoc jars, gpg sign, central-publishing-maven-plugin with autoPublish) - per-module on every deployable artifact (Central requires name/description/url/license/scm/developer); benchmark + integration-tests already set maven.deploy.skip - .github/workflows/publish.yml: on a v* tag, build (checkout submodule + Zig), deploy -Prelease, then cut a GitHub release from CHANGELOG.md - CHANGELOG.md with the 0.1 notes Release: set repo secrets CENTRAL_USERNAME/PASSWORD, GPG_PRIVATE_KEY, GPG_PASSPHRASE; register io.github.dfa1 on central.sonatype.com; push tag v0.1. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/publish.yml | 54 +++++++++++++++++++++ CHANGELOG.md | 33 +++++++++++++ bom/pom.xml | 1 + native/linux-aarch64/pom.xml | 1 + native/linux-x86_64/pom.xml | 1 + native/osx-aarch64/pom.xml | 1 + native/osx-x86_64/pom.xml | 1 + native/windows-aarch64/pom.xml | 1 + native/windows-x86_64/pom.xml | 1 + pom.xml | 88 ++++++++++++++++++++++++++++++++++ zstd/pom.xml | 1 + 11 files changed, 183 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 CHANGELOG.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ec24210 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,54 @@ +name: Publish to Maven Central + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + environment: maven + steps: + - name: Checkout (with zstd submodule) + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up JDK 25 with Maven Central publishing + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'zulu' + cache: 'maven' + server-id: central + server-username: CENTRAL_USERNAME + server-password: CENTRAL_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Set up Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + - name: Publish to Maven Central + env: + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: ./mvnw deploy -Prelease -DskipTests --batch-mode -ntp + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${GITHUB_REF_NAME#v}" + NOTES=$(awk "/^## \[$VERSION\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md) + gh release create "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --notes "$NOTES" \ + --verify-tag diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e6eb741 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project are documented here. Format loosely follows +[Keep a Changelog](https://keepachangelog.com/); versions are released as `v*` +git tags, which trigger publication to Maven Central. + +## [0.1] + +First release. Java 25 Foreign Function & Memory (FFM) bindings for +[Zstandard](https://github.com/facebook/zstd) 1.5.7, built hermetically from +vendored source with `zig cc` (no JNI, no prebuilt binaries). 68 of the public +zstd symbols are bound; see `docs/supported.md`. + +### Added +- One-shot compression/decompression over `byte[]` and zero-copy `MemorySegment` + (`Zstd`, `ZstdCompressCtx`, `ZstdDecompressCtx`). +- Dictionaries: training (`ZDICT_trainFromBuffer`, COVER / fast-COVER optimisers, + `finalizeDictionary`), digested `ZstdCompressDict` / `ZstdDecompressDict`, + dictionary ids and header size. +- Streaming: `ZstdOutputStream` / `ZstdInputStream` (java.io) and a zero-copy + `MemorySegment` driver (`ZstdCompressStream` / `ZstdDecompressStream`), with + dictionaries, `pledgedSrcSize`, and live `progress()`. +- All advanced parameters (`ZstdCompressParameter` / `ZstdDecompressParameter`) + with bounds queries; checksum, long-distance matching, window log, etc. +- Frame inspection (`ZstdFrame`): header, content/compressed size, dictionary id, + skippable frames. +- Typed errors (`ZstdException.code()` / `ZstdErrorCode`) and memory accounting + (`sizeOf()`, `Zstd.estimate*Size`). +- Native artifacts for macOS, Linux and Windows on x86_64 and aarch64, + cross-compiled from a single host with `zig cc`. +- Format-compatibility tests against the reference zstd-jni binding. + +[0.1]: https://github.com/dfa1/zstd-java/releases/tag/v0.1 diff --git a/bom/pom.xml b/bom/pom.xml index ba791e0..1769bbf 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -12,6 +12,7 @@ zstd-java-bom pom zstd FFM BOM + Bill of materials for the zstd-java modules diff --git a/native/linux-aarch64/pom.xml b/native/linux-aarch64/pom.xml index ba4e575..5fb37e4 100644 --- a/native/linux-aarch64/pom.xml +++ b/native/linux-aarch64/pom.xml @@ -11,6 +11,7 @@ zstd-java-native-linux-aarch64 zstd FFM Native linux-aarch64 + Native zstd library for Linux aarch64 jar diff --git a/native/linux-x86_64/pom.xml b/native/linux-x86_64/pom.xml index 830bca4..8a0ef81 100644 --- a/native/linux-x86_64/pom.xml +++ b/native/linux-x86_64/pom.xml @@ -11,6 +11,7 @@ zstd-java-native-linux-x86_64 zstd FFM Native linux-x86_64 + Native zstd library for Linux x86_64 jar diff --git a/native/osx-aarch64/pom.xml b/native/osx-aarch64/pom.xml index 0e86ac8..588d627 100644 --- a/native/osx-aarch64/pom.xml +++ b/native/osx-aarch64/pom.xml @@ -11,6 +11,7 @@ zstd-java-native-osx-aarch64 zstd FFM Native osx-aarch64 + Native zstd library for macOS aarch64 jar diff --git a/native/osx-x86_64/pom.xml b/native/osx-x86_64/pom.xml index 2c4f9fe..3d6a439 100644 --- a/native/osx-x86_64/pom.xml +++ b/native/osx-x86_64/pom.xml @@ -11,6 +11,7 @@ zstd-java-native-osx-x86_64 zstd FFM Native osx-x86_64 + Native zstd library for macOS x86_64 jar diff --git a/native/windows-aarch64/pom.xml b/native/windows-aarch64/pom.xml index a13789a..8b6e707 100644 --- a/native/windows-aarch64/pom.xml +++ b/native/windows-aarch64/pom.xml @@ -11,6 +11,7 @@ zstd-java-native-windows-aarch64 zstd FFM Native windows-aarch64 + Native zstd library for Windows aarch64 jar diff --git a/native/windows-x86_64/pom.xml b/native/windows-x86_64/pom.xml index f05557c..1b0065e 100644 --- a/native/windows-x86_64/pom.xml +++ b/native/windows-x86_64/pom.xml @@ -11,6 +11,7 @@ zstd-java-native-windows-x86_64 zstd FFM Native windows-x86_64 + Native zstd library for Windows x86_64 jar diff --git a/pom.xml b/pom.xml index c4ebb5a..d155eb9 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,13 @@ + + scm:git:https://github.com/dfa1/zstd-java.git + scm:git:git@github.com:dfa1/zstd-java.git + https://github.com/dfa1/zstd-java + HEAD + + native/osx-aarch64 native/osx-x86_64 @@ -43,6 +50,10 @@ UTF-8 25 + 0.11.0 + 3.2.8 + 3.4.0 + 3.12.0 @@ -186,4 +197,81 @@ + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + none + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-plugin.version} + true + + central + true + published + + + + + + diff --git a/zstd/pom.xml b/zstd/pom.xml index 22f7ee6..70bda93 100644 --- a/zstd/pom.xml +++ b/zstd/pom.xml @@ -11,6 +11,7 @@ zstd-java zstd FFM Core + Java Foreign Function & Memory (FFM) bindings for zstd jar From a110d8bcdb9b48d401aac920700d5858f48b1d65 Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Fri, 26 Jun 2026 12:38:36 +0200 Subject: [PATCH 2/4] fix(stream): detect truncated frames; drop per-byte allocations ZstdInputStream silently returned clean EOF when the underlying stream ended mid-frame, losing data without error. Track zstd's outstanding-input hint from decompressStream and throw ZstdException on a non-zero hint at EOF. Empty input stays a clean EOF. Also reuse a single-byte scratch buffer in read()/write(int) instead of allocating new byte[1] per call (streams are already non-thread-safe). Co-Authored-By: Claude Opus 4.8 --- .../io/github/dfa1/zstd/ZstdInputStream.java | 33 ++++++++++++++----- .../io/github/dfa1/zstd/ZstdOutputStream.java | 4 ++- .../io/github/dfa1/zstd/ZstdStreamTest.java | 32 ++++++++++++++++++ 3 files changed, 60 insertions(+), 9 deletions(-) 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 435b400..cb6d246 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdInputStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdInputStream.java @@ -35,10 +35,14 @@ public final class ZstdInputStream extends InputStream { private final ZstdStreamBuffer outBufView = new ZstdStreamBuffer(arena); private final byte[] feed; private final byte[] hold; + private final byte[] single = new byte[1]; private int holdStart; private int holdEnd; private boolean inputEof; private boolean closed; + /// zstd's "bytes still expected" hint from the last decompressStream call; + /// non-zero at EOF means the final frame was truncated. + private long lastHint; /// Wraps `in`, decompressing the zstd frame it carries. /// @@ -84,9 +88,8 @@ private void loadDictionary(ZstdDictionary dictionary) { @Override public int read() throws IOException { - byte[] one = new byte[1]; - int n = read(one, 0, 1); - return n == -1 ? -1 : (one[0] & 0xFF); + int n = read(single, 0, 1); + return n == -1 ? -1 : (single[0] & 0xFF); } @Override @@ -112,13 +115,20 @@ private boolean produce() throws IOException { int r = inputEof ? -1 : in.read(feed); if (r == -1) { inputEof = true; + // The frame boundary is clean only when the last decompressStream + // call reported nothing outstanding; otherwise the stream was cut + // mid-frame and the remaining bytes are lost. + if (lastHint != 0) { + throw new ZstdException("truncated zstd stream: " + lastHint + + " more input byte(s) expected"); + } return false; } MemorySegment.copy(feed, 0, inSeg, JAVA_BYTE, 0, r); inBuf.set(inSeg, r, 0); } outBufView.set(outSeg, outCap, 0); - Zstd.call(() -> (long) Bindings.DECOMPRESS_STREAM.invokeExact( + lastHint = Zstd.call(() -> (long) Bindings.DECOMPRESS_STREAM.invokeExact( dctx, outBufView.segment(), inBuf.segment())); int produced = Math.toIntExact(outBufView.pos()); if (produced > 0) { @@ -127,10 +137,17 @@ private boolean produce() throws IOException { holdEnd = produced; return true; } - // nothing produced: if input is exhausted and the underlying stream is - // at EOF, there is no more output to come. - if (inputEof && inBuf.pos() == inBuf.size()) { - return false; + // Nothing produced. If the decoder neither advanced its input nor wants + // more, it cannot make progress on this input — stop to avoid spinning. + if (inBuf.pos() == inBuf.size()) { + if (inputEof) { + if (lastHint != 0) { + throw new ZstdException("truncated zstd stream: " + lastHint + + " more input byte(s) expected"); + } + return false; + } + // input drained but frame wants more: loop to refill from `in`. } } } 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 84f8c80..db8cedb 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java @@ -41,6 +41,7 @@ public final class ZstdOutputStream extends OutputStream { private final ZstdStreamBuffer in = new ZstdStreamBuffer(arena); private final ZstdStreamBuffer outBuf = new ZstdStreamBuffer(arena); private final byte[] drain; + private final byte[] single = new byte[1]; private boolean closed; /// Wraps `out`, compressing at the library default level. @@ -124,7 +125,8 @@ private void loadDictionary(ZstdDictionary dictionary) { @Override public void write(int b) throws IOException { - write(new byte[]{(byte) b}, 0, 1); + single[0] = (byte) b; + write(single, 0, 1); } @Override diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java index 4e28b64..8937c1f 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java @@ -12,6 +12,7 @@ import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class ZstdStreamTest { @@ -148,6 +149,37 @@ private byte[] record(int i) { } } + @Nested + class Truncation { + + @ParameterizedTest + @ValueSource(ints = {1, 8, 64}) + void throwsWhenFinalFrameIsCutShort(int bytesDropped) throws IOException { + // Given a valid frame with its tail bytes lopped off (random data so the + // frame stays large enough to drop bytes from) + byte[] original = randomBytes(50_000); + byte[] frame = streamCompress(original, 6); + byte[] cut = java.util.Arrays.copyOf(frame, frame.length - bytesDropped); + + // When the streaming decompressor drains it + // Then it reports the truncation instead of returning a clean EOF + try (ZstdInputStream zin = new ZstdInputStream(new ByteArrayInputStream(cut))) { + assertThatThrownBy(zin::readAllBytes) + .isInstanceOf(ZstdException.class) + .hasMessageContaining("truncated"); + } + } + + @Test + void emptyInputIsCleanEofNotTruncation() throws IOException { + // Given no input at all + // When read, it is end-of-stream, not an error + try (ZstdInputStream zin = new ZstdInputStream(new ByteArrayInputStream(new byte[0]))) { + assertThat(zin.read()).isEqualTo(-1); + } + } + } + @Nested class PledgedSize { From 2bcdf32e67b8e58cb23e27571982fd01a4a570e8 Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Fri, 26 Jun 2026 12:41:52 +0200 Subject: [PATCH 3/4] refactor: guard zero-copy heap segments; model ZDICT cover params as layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-copy MemorySegment APIs dereference the segment address in C, so a heap-backed segment crashed with a cryptic FFM linker error. Add an isNative() guard (Zstd.requireNative) at each zero-copy entry — compress /decompress on the contexts and stream, plus decompressedSize — failing fast with a clear message. Replace the hand-coded ZDICT_cover/fastCover param struct offsets in optimize() with named MemoryLayout structs; offsets and allocation size now derive from the layout instead of magic numbers. Co-Authored-By: Claude Opus 4.8 --- pom.xml | 12 +++++ .../main/java/io/github/dfa1/zstd/Zstd.java | 12 +++++ .../io/github/dfa1/zstd/ZstdCompressCtx.java | 4 ++ .../github/dfa1/zstd/ZstdCompressStream.java | 2 + .../github/dfa1/zstd/ZstdDecompressCtx.java | 4 ++ .../io/github/dfa1/zstd/ZstdDictionary.java | 45 ++++++++++++++++--- .../io/github/dfa1/zstd/ZstdSegmentTest.java | 40 +++++++++++++++++ 7 files changed, 114 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index d155eb9..fa9b6af 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ 3.2.8 3.4.0 3.12.0 + 3.3.1 @@ -175,6 +176,17 @@ + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + + true + v@{project.version} + false + true + + 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 f7ba90c..98495f0 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java @@ -91,6 +91,7 @@ public static byte[] decompress(byte[] compressed, int maxSize) { /// @return the decompressed length in bytes /// @throws ZstdException if the frame is invalid or does not store its size public static long decompressedSize(MemorySegment frame) { + requireNative(frame, "frame"); long size; try { size = (long) Bindings.GET_FRAME_CONTENT_SIZE.invokeExact(frame, frame.byteSize()); @@ -275,6 +276,17 @@ private static String errorName(long code) { } } + /// Guards a zero-copy entry point: the segment handed to zstd must be backed + /// 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 MemorySegment requireNative(MemorySegment seg, String name) { + if (!seg.isNative()) { + throw new IllegalArgumentException( + name + " must be a native (off-heap) MemorySegment; got a heap segment"); + } + return seg; + } + static MemorySegment copyIn(Arena arena, byte[] src) { MemorySegment seg = arena.allocate(Math.max(src.length, 1)); MemorySegment.copy(src, 0, seg, JAVA_BYTE, 0, src.length); 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 f69aef1..1ce8c82 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressCtx.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressCtx.java @@ -162,6 +162,8 @@ public byte[] compress(byte[] src, ZstdCompressDict dict) { /// @return the number of bytes written into `dst` /// @throws ZstdException if `dst` is too small or compression fails public long compress(MemorySegment dst, MemorySegment src) { + Zstd.requireNative(dst, "dst"); + Zstd.requireNative(src, "src"); return Zstd.call(() -> (long) Bindings.COMPRESS2.invokeExact( ptr(), dst, dst.byteSize(), src, src.byteSize())); } @@ -173,6 +175,8 @@ public long compress(MemorySegment dst, MemorySegment src) { /// @param dict the pre-digested compression dictionary /// @return the number of bytes written into `dst` public long compress(MemorySegment dst, MemorySegment src, ZstdCompressDict dict) { + Zstd.requireNative(dst, "dst"); + Zstd.requireNative(src, "src"); MemorySegment cdict = dict.ptr(); return Zstd.call(() -> (long) Bindings.COMPRESS_USING_CDICT.invokeExact( ptr(), dst, dst.byteSize(), src, src.byteSize(), cdict)); 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 44c7443..3216e04 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java @@ -86,6 +86,8 @@ private static MemorySegment create(int level, ZstdDictionary dictionary) { /// @return how much was consumed and produced, and the remaining hint /// @throws ZstdException if compression fails public ZstdStreamResult compress(MemorySegment dst, MemorySegment src, ZstdEndDirective directive) { + Zstd.requireNative(dst, "dst"); + Zstd.requireNative(src, "src"); in.set(src, src.byteSize(), 0); out.set(dst, dst.byteSize(), 0); long remaining = Zstd.call(() -> (long) Bindings.COMPRESS_STREAM2.invokeExact( 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 8ed9d6c..23bde76 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressCtx.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressCtx.java @@ -115,6 +115,8 @@ public byte[] decompress(byte[] compressed, int maxSize, ZstdDecompressDict dict /// @return the number of bytes written into `dst` /// @throws ZstdException if `dst` is too small or the frame is invalid public long decompress(MemorySegment dst, MemorySegment src) { + Zstd.requireNative(dst, "dst"); + Zstd.requireNative(src, "src"); return Zstd.call(() -> (long) Bindings.DECOMPRESS_DCTX.invokeExact( ptr(), dst, dst.byteSize(), src, src.byteSize())); } @@ -126,6 +128,8 @@ public long decompress(MemorySegment dst, MemorySegment src) { /// @param dict the pre-digested decompression dictionary /// @return the number of bytes written into `dst` public long decompress(MemorySegment dst, MemorySegment src, ZstdDecompressDict dict) { + Zstd.requireNative(dst, "dst"); + Zstd.requireNative(src, "src"); MemorySegment ddict = dict.ptr(); return Zstd.call(() -> (long) Bindings.DECOMPRESS_USING_DDICT.invokeExact( ptr(), dst, dst.byteSize(), src, src.byteSize(), ddict)); 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 380c5af..0623a6a 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java @@ -1,12 +1,15 @@ package io.github.dfa1.zstd; import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemoryLayout.PathElement; import java.lang.foreign.MemorySegment; import java.lang.invoke.MethodHandle; import java.nio.charset.StandardCharsets; import java.util.List; import static java.lang.foreign.ValueLayout.JAVA_BYTE; +import static java.lang.foreign.ValueLayout.JAVA_DOUBLE; import static java.lang.foreign.ValueLayout.JAVA_INT; import static java.lang.foreign.ValueLayout.JAVA_LONG; @@ -28,6 +31,39 @@ /// } public final class ZstdDictionary { + // ZDICT_cover_params_t { unsigned k,d,steps,nbThreads; double splitPoint; + // unsigned shrinkDict, shrinkDictMaxRegression; ZDICT_params_t zParams; } + // ZDICT_params_t { int compressionLevel; unsigned notificationLevel, dictID; } + private static final MemoryLayout COVER_PARAMS = MemoryLayout.structLayout( + JAVA_INT.withName("k"), + JAVA_INT.withName("d"), + JAVA_INT.withName("steps"), + JAVA_INT.withName("nbThreads"), + JAVA_DOUBLE.withName("splitPoint"), + JAVA_INT.withName("shrinkDict"), + JAVA_INT.withName("shrinkDictMaxRegression"), + JAVA_INT.withName("compressionLevel"), + JAVA_INT.withName("notificationLevel"), + JAVA_INT.withName("dictID"), + MemoryLayout.paddingLayout(4)); // trailing pad to the C struct's 8-byte alignment + + // ZDICT_fastCover_params_t adds `unsigned f` after d and `unsigned accel` + // after splitPoint; the 4-byte gap before the 8-aligned double is explicit. + private static final MemoryLayout FASTCOVER_PARAMS = MemoryLayout.structLayout( + JAVA_INT.withName("k"), + JAVA_INT.withName("d"), + JAVA_INT.withName("f"), + JAVA_INT.withName("steps"), + JAVA_INT.withName("nbThreads"), + MemoryLayout.paddingLayout(4), + JAVA_DOUBLE.withName("splitPoint"), + JAVA_INT.withName("accel"), + JAVA_INT.withName("shrinkDict"), + JAVA_INT.withName("shrinkDictMaxRegression"), + JAVA_INT.withName("compressionLevel"), + JAVA_INT.withName("notificationLevel"), + JAVA_INT.withName("dictID")); + private final byte[] bytes; private ZstdDictionary(byte[] bytes) { @@ -154,11 +190,10 @@ private static ZstdDictionary optimize(List samples, int maxDictBytes, offset += s.length; } // zeroed params (auto-tune k/d/steps); set single-threaded + target level. - // fastCover: nbThreads@16, compressionLevel@44, size 56. - // cover: nbThreads@12, compressionLevel@32, size 48. - MemorySegment params = arena.allocate(fast ? 56 : 48); - params.set(JAVA_INT, fast ? 16 : 12, 1); - params.set(JAVA_INT, fast ? 44 : 32, compressionLevel); + MemoryLayout layout = fast ? FASTCOVER_PARAMS : COVER_PARAMS; + MemorySegment params = arena.allocate(layout); + params.set(JAVA_INT, layout.byteOffset(PathElement.groupElement("nbThreads")), 1); + params.set(JAVA_INT, layout.byteOffset(PathElement.groupElement("compressionLevel")), compressionLevel); MethodHandle handle = fast ? Bindings.ZDICT_OPTIMIZE_FASTCOVER : Bindings.ZDICT_OPTIMIZE_COVER; MemorySegment dictBuf = arena.allocate(maxDictBytes); long produced; diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentTest.java index cc9ebe9..7f43296 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentTest.java @@ -11,6 +11,7 @@ import static java.lang.foreign.ValueLayout.JAVA_BYTE; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class ZstdSegmentTest { @@ -97,6 +98,45 @@ void roundTripsWithDigestedDictionary() { } } + @Nested + class HeapSegmentGuard { + + @Test + void compressRejectsHeapSource() { + // Given a heap-backed segment (not off-heap) handed to the zero-copy API + MemorySegment heap = MemorySegment.ofArray(new byte[64]); + try (Arena arena = Arena.ofConfined(); + ZstdCompressCtx cctx = new ZstdCompressCtx()) { + MemorySegment dst = arena.allocate(64); + + // Then it fails fast with a clear message instead of a cryptic FFM error + assertThatThrownBy(() -> cctx.compress(dst, heap)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("native"); + } + } + + @Test + void decompressRejectsHeapDestination() { + MemorySegment heapDst = MemorySegment.ofArray(new byte[64]); + try (Arena arena = Arena.ofConfined(); + ZstdDecompressCtx dctx = new ZstdDecompressCtx()) { + MemorySegment src = arena.allocate(64); + + assertThatThrownBy(() -> dctx.decompress(heapDst, src)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("native"); + } + } + + @Test + void decompressedSizeRejectsHeapFrame() { + assertThatThrownBy(() -> Zstd.decompressedSize(MemorySegment.ofArray(new byte[8]))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("native"); + } + } + private static MemorySegment segmentOf(Arena arena, byte[] bytes) { MemorySegment seg = arena.allocate(Math.max(bytes.length, 1)); MemorySegment.copy(bytes, 0, seg, JAVA_BYTE, 0, bytes.length); From 004bdbd3989f333558b7f52f743213d2c6298f1e Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Fri, 26 Jun 2026 12:42:10 +0200 Subject: [PATCH 4/4] build: add maven-release-plugin for tagging releases Mirror vortex-java: release:prepare drops -SNAPSHOT, tags v (pushChanges=false, autoVersionSubmodules), bumps to next snapshot. Pushing the v* tag triggers publish.yml -> deploy -Prelease to Maven Central. Document the flow in the release profile comment. Co-Authored-By: Claude Opus 4.8 --- pom.xml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index fa9b6af..891dfe6 100644 --- a/pom.xml +++ b/pom.xml @@ -211,13 +211,18 @@ - + 2. A Central user token (Account / User Tokens); in CI it comes from + the CENTRAL_USERNAME / CENTRAL_PASSWORD secrets. + 3. A published GPG key (GPG_PRIVATE_KEY / GPG_PASSPHRASE secrets). --> release