Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions .github/smoke/src/test/java/SmokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import io.github.dfa1.zstd.ZstdFrameProgression;
import io.github.dfa1.zstd.ZstdFrameType;
import io.github.dfa1.zstd.ZstdInputStream;
import io.github.dfa1.zstd.ZstdMagicVariant;
import io.github.dfa1.zstd.ZstdOutputStream;
import io.github.dfa1.zstd.ZstdResetDirective;
import io.github.dfa1.zstd.ZstdSkippableContent;
import io.github.dfa1.zstd.ZstdStreamResult;
import io.github.dfa1.zstd.ZstdWindowLog;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -74,8 +76,8 @@ static void trainDictionary() {

@Test
void versionAndSizing() {
check(!Zstd.version().isBlank(), "version() returned blank");
check(Zstd.versionNumber() > 0, "versionNumber() not positive");
check(!Zstd.version().toString().isBlank(), "version() returned blank");
check(Zstd.version().number() > 0, "version().number() not positive");

int min = ZstdCompressionLevel.FASTEST.value();
int max = ZstdCompressionLevel.MAX.value();
Expand Down Expand Up @@ -168,14 +170,14 @@ void frameIntrospection() {
check(ZstdFrame.isZstdFrame(compressed), "isZstdFrame(byte[]) false for a real frame");
check(!ZstdFrame.isZstdFrame("plain text, not zstd".getBytes(StandardCharsets.UTF_8)),
"isZstdFrame(byte[]) true for non-zstd data");
check(ZstdFrame.compressedSize(compressed) == compressed.length, "compressedSize(byte[]) mismatch");
check(ZstdFrame.compressedSize(compressed).value() == compressed.length, "compressedSize(byte[]) mismatch");
check(ZstdFrame.decompressedSize(compressed).value() == original.length, "decompressedSize(byte[]) mismatch");
check(ZstdFrame.decompressedBound(compressed).value() >= original.length, "decompressedBound(byte[]) too small");
check(ZstdFrame.decompressionMargin(compressed) >= 0, "decompressionMargin(byte[]) negative");
check(ZstdFrame.decompressionMargin(compressed).value() >= 0, "decompressionMargin(byte[]) negative");
check(!ZstdFrame.dictId(compressed).isPresent(), "dictId(byte[]) expected NONE for a non-dictionary frame");
check(!ZstdFrame.isSkippableFrame(compressed), "isSkippableFrame(byte[]) true for a standard frame");

long headerSize = ZstdFrame.headerSize(compressed);
long headerSize = ZstdFrame.headerSize(compressed).value();
check(headerSize > 0 && headerSize <= compressed.length, "headerSize(byte[]) out of range");

ZstdFrameHeader header = ZstdFrame.header(compressed);
Expand All @@ -189,14 +191,14 @@ void frameIntrospection() {
@Test
void skippableFrames() {
byte[] payload = "smoke-test skippable payload".getBytes(StandardCharsets.UTF_8);
byte[] frame = ZstdFrame.writeSkippableFrame(payload, 3);
byte[] frame = ZstdFrame.writeSkippableFrame(payload, new ZstdMagicVariant(3));

check(ZstdFrame.isSkippableFrame(frame), "isSkippableFrame(byte[]) false for a written skippable frame");
check(ZstdFrame.isZstdFrame(frame), "isZstdFrame(byte[]) should be true for a skippable frame too");

ZstdSkippableContent read = ZstdFrame.readSkippableFrame(frame);
checkArrayEquals(read.content(), payload, "readSkippableFrame() content mismatch");
check(read.magicVariant() == 3, "readSkippableFrame() magicVariant mismatch");
check(read.magicVariant().value() == 3, "readSkippableFrame() magicVariant mismatch");
}

@Test
Expand All @@ -206,7 +208,7 @@ void compressContextAdvancedParameters() {
cctx.level(new ZstdCompressionLevel(5))
.checksum(true)
.longDistanceMatching(true)
.windowLog(20)
.windowLog(new ZstdWindowLog(20))
.parameter(ZstdCompressParameter.STRATEGY, 3);

byte[] compressed = cctx.compress(original);
Expand All @@ -224,10 +226,10 @@ void compressContextAdvancedParameters() {
@Test
void decompressContextAdvanced() {
byte[] original = sampleText();
try (ZstdCompressContext cctx = new ZstdCompressContext().windowLog(23);
try (ZstdCompressContext cctx = new ZstdCompressContext().windowLog(new ZstdWindowLog(23));
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
dctx.windowLogMax(24);
byte[] restored = dctx.decompress(cctx.compress(original), original.length);
dctx.windowLogMax(new ZstdWindowLog(24));
byte[] restored = dctx.decompress(cctx.compress(original), new ZstdByteSize(original.length));
checkArrayEquals(original, restored, "windowLogMax() decompress round-trip mismatch");
check(dctx.sizeOf().value() > 0, "dctx.sizeOf() not positive");

Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ git tags, which trigger publication to Maven Central.

## [Unreleased]

### Added
- `ZstdDecompressContext.decompress(byte[])` — decompresses a trusted frame using
the decompressed size stored in its header, mirroring the static
`Zstd.decompress(byte[])`. For untrusted input keep the bounded
`decompress(byte[], ZstdByteSize)` overload, now documented as the safe entry
point.
- `ZstdWindowLog` and `ZstdMagicVariant` value types, validated at construction —
the window log against the linked libzstd's accepted range (with `0` /
`ZstdWindowLog.AUTO` for "library chooses"), the magic variant to `0..15`.
- `ZstdVersion` value type — a comparable `(major, minor, patch)` with `number()`
(the packed form) and `toString()` (`x.y.z`), plus `isAtLeast(int, int, int)`
for feature-gating against the linked libzstd.

### Changed
- **Breaking:** the compression-level bound queries `Zstd.maxCompressionLevel()`,
`minCompressionLevel()`, and `defaultCompressionLevel()` are no longer public.
Expand All @@ -17,6 +30,25 @@ git tags, which trigger publication to Maven Central.
throws `ZstdException` (was `ArithmeticException`) when the size exceeds the
maximum array length — the narrowing happens at the API boundary, so callers
no longer catch a raw `ArithmeticException`.
- **Breaking:** `ZstdDecompressContext.decompress` now takes a `ZstdByteSize`
bound instead of a naked `int` (all three overloads: plain, `+ ZstdDictionary`,
`+ ZstdDecompressDictionary`), completing the `ZstdByteSize` migration. Wrap the
bound in `new ZstdByteSize(n)`.
- **Breaking:** the frame-size probes `ZstdFrame.compressedSize`, `headerSize`,
and `decompressionMargin` now return `ZstdByteSize` instead of `long`, and
`ZstdDictionary.size()`/`headerSize()` return `ZstdByteSize` instead of `int`,
matching the other size accessors — call `.value()` for the raw number. The
zero-copy segment `compress`/`decompress` overloads still return a raw `long`
by design (the count feeds straight into `MemorySegment.asSlice`).
- **Breaking:** `ZstdCompressContext.windowLog` and
`ZstdDecompressContext.windowLogMax` now take a `ZstdWindowLog`, and
`ZstdFrame.writeSkippableFrame` / `ZstdSkippableContent.magicVariant()` now use
`ZstdMagicVariant`. Wrap the raw `int` (`new ZstdWindowLog(n)` /
`new ZstdMagicVariant(n)`), or use `ZstdWindowLog.AUTO`.
- **Breaking:** `Zstd.version()` now returns a `ZstdVersion` instead of a
`String`, and `Zstd.versionNumber()` is removed. Use
`Zstd.version().toString()` for the `x.y.z` string and
`Zstd.version().number()` for the packed number.

## [0.11] - 2026-07-24

Expand Down
2 changes: 1 addition & 1 deletion docs/supported.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ rather than the deprecated `ZSTD_getDecompressedSize`.
|---|---|
| `ZSTD_compress`, `ZSTD_decompress`, `ZSTD_compressBound` | `Zstd.compress` / `decompress` / `compressBound` |
| `ZSTD_maxCLevel`, `ZSTD_minCLevel`, `ZSTD_defaultCLevel` | `ZstdCompressionLevel.MAX` / `FASTEST` / `DEFAULT` |
| `ZSTD_versionNumber`, `ZSTD_versionString` | `Zstd.version` |
| `ZSTD_versionNumber`, `ZSTD_versionString` | `Zstd.version` (returns `ZstdVersion`) |
| `ZSTD_isError`, `ZSTD_getErrorName` | internal error mapping in `Zstd` |
| `ZSTD_getFrameContentSize` | `Zstd.decompress(byte[])`, `Zstd.decompressedSize` |
| `ZSTD_createCCtx`, `ZSTD_freeCCtx`, `ZSTD_compressCCtx` | `ZstdCompressContext` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.github.dfa1.zstd.ZstdFrameHeader;
import io.github.dfa1.zstd.ZstdFrameType;
import io.github.dfa1.zstd.ZstdInputStream;
import io.github.dfa1.zstd.ZstdMagicVariant;
import io.github.dfa1.zstd.ZstdOutputStream;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -128,7 +129,7 @@ void jniStreamSkipsJavaSkippableFrame() {
// Given
byte[] payload = "after the skippable frame ".repeat(1000).getBytes(StandardCharsets.UTF_8);
byte[] meta = "sidecar-metadata".getBytes(StandardCharsets.UTF_8);
byte[] skippable = ZstdFrame.writeSkippableFrame(meta, 0);
byte[] skippable = ZstdFrame.writeSkippableFrame(meta, new ZstdMagicVariant(0));
byte[] real = Zstd.compress(payload, ZstdCompressionLevel.DEFAULT);

// When
Expand All @@ -142,7 +143,7 @@ void jniStreamSkipsJavaSkippableFrame() {
void javaParsesItsOwnSkippableFrameHeader() {
// Given
byte[] meta = "sidecar".getBytes(StandardCharsets.UTF_8);
byte[] skippable = ZstdFrame.writeSkippableFrame(meta, 5);
byte[] skippable = ZstdFrame.writeSkippableFrame(meta, new ZstdMagicVariant(5));

// When
ZstdFrameHeader header = ZstdFrame.header(skippable);
Expand Down
4 changes: 0 additions & 4 deletions zstd/src/main/java/io/github/dfa1/zstd/Bindings.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ final class Bindings {
static final MethodHandle VERSION_NUMBER =
NativeLibrary.lookup("ZSTD_versionNumber", FunctionDescriptor.of(JAVA_INT));

// const char* ZSTD_versionString(void)
static final MethodHandle VERSION_STRING =
NativeLibrary.lookup("ZSTD_versionString", FunctionDescriptor.of(ADDRESS));

// --- reusable contexts ---

// ZSTD_CCtx* ZSTD_createCCtx(void)
Expand Down
25 changes: 7 additions & 18 deletions zstd/src/main/java/io/github/dfa1/zstd/Zstd.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

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;
Expand Down Expand Up @@ -222,26 +221,16 @@ public static ZstdByteSize estimateDecompressDictSize(ZstdByteSize dictSize) {
}
}

/// Runtime zstd version, e.g. `"1.6.0"`.
/// Runtime version of the linked zstd library, e.g. `1.6.0`.
///
/// @return the linked zstd library version as an `x.y.z` string
@SuppressWarnings("restricted") // reinterpret needed to read a C string of unknown length
public static String version() {
try {
MemorySegment p = (MemorySegment) Bindings.VERSION_STRING.invokeExact();
return p.reinterpret(Long.MAX_VALUE).getString(0, StandardCharsets.US_ASCII);
} catch (Throwable t) {
throw NativeCall.rethrow(t);
}
}

/// Runtime zstd version as a single number for programmatic comparison,
/// encoded `MAJOR * 10000 + MINOR * 100 + PATCH` — e.g. `10507` for `1.5.7`.
/// Read [ZstdVersion#toString()] for the `x.y.z` string, [ZstdVersion#number()]
/// for the packed number, or compare it (e.g. [ZstdVersion#isAtLeast(int, int, int)])
/// to feature-gate against the runtime library.
///
/// @return the linked zstd library version number
public static int versionNumber() {
/// @return the linked zstd library version
public static ZstdVersion version() {
try {
return (int) Bindings.VERSION_NUMBER.invokeExact();
return ZstdVersion.ofNumber((int) Bindings.VERSION_NUMBER.invokeExact());
} catch (Throwable t) {
throw NativeCall.rethrow(t);
}
Expand Down
10 changes: 8 additions & 2 deletions zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ public ZstdCompressContext longDistanceMatching(boolean enabled) {
///
/// @param windowLog the base-2 log of the window size
/// @return `this`, for chaining
public ZstdCompressContext windowLog(int windowLog) {
return parameter(ZstdCompressParameter.WINDOW_LOG, windowLog);
public ZstdCompressContext windowLog(ZstdWindowLog windowLog) {
Objects.requireNonNull(windowLog, "windowLog");
return parameter(ZstdCompressParameter.WINDOW_LOG, windowLog.value());
}

/// Resets this context so it can be reused for the next frame without the
Expand Down Expand Up @@ -288,6 +289,11 @@ public byte[] compress(byte[] src, ZstdCompressDictionary dict) {
///
/// Size `dst` with [Zstd#compressBound(ZstdByteSize)] to guarantee it fits.
///
/// Returns a raw `long` rather than a [ZstdByteSize]: on this zero-copy path
/// the byte count feeds straight into [MemorySegment#asSlice(long, long)], so
/// it is left unboxed by design to keep the fast path allocation-free. The
/// heap `byte[]` overloads, which size an array, return richer types.
///
/// @param dst the native destination buffer to write the frame into
/// @param src the native source bytes to compress
/// @return the number of bytes written into `dst`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ public ZstdDecompressContext parameter(ZstdDecompressParameter parameter, int va
///
/// @param windowLogMax the base-2 log of the maximum accepted window size
/// @return `this`, for chaining
public ZstdDecompressContext windowLogMax(int windowLogMax) {
return parameter(ZstdDecompressParameter.WINDOW_LOG_MAX, windowLogMax);
public ZstdDecompressContext windowLogMax(ZstdWindowLog windowLogMax) {
Objects.requireNonNull(windowLogMax, "windowLogMax");
return parameter(ZstdDecompressParameter.WINDOW_LOG_MAX, windowLogMax.value());
}

/// Resets this context so it can be reused for the next frame without the
Expand Down Expand Up @@ -247,6 +248,11 @@ public byte[] decompress(byte[] compressed, ZstdByteSize maxSize, ZstdDecompress
/// Size `dst` to the decompressed length (read it from the frame with
/// [Zstd#decompress(byte[])]'s header logic, or known out-of-band).
///
/// Returns a raw `long` rather than a [ZstdByteSize]: on this zero-copy path
/// the byte count feeds straight into [MemorySegment#asSlice(long, long)], so
/// it is left unboxed by design to keep the fast path allocation-free. The
/// heap `byte[]` overloads, which size an array, return richer types.
///
/// @param dst the native destination buffer to write the result into
/// @param src the native source frame to decompress
/// @return the number of bytes written into `dst`
Expand Down
8 changes: 4 additions & 4 deletions zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,14 @@ public ZstdDictionaryId id() {
///
/// @return the header size in bytes
/// @throws ZstdException if this is not a valid zstd dictionary
public int headerSize() {
public ZstdByteSize headerSize() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment seg = Zstd.copyIn(arena, bytes);
long size = (long) Bindings.ZDICT_GET_DICT_HEADER_SIZE.invokeExact(seg, (long) bytes.length);
if (zdictIsError(size)) {
throw new ZstdException("not a valid dictionary: " + zdictErrorName(size));
}
return Math.toIntExact(size);
return new ZstdByteSize(size);
} catch (Throwable t) {
throw NativeCall.rethrow(t);
}
Expand All @@ -307,8 +307,8 @@ public byte[] toByteArray() {
/// The size of this dictionary.
///
/// @return the dictionary size in bytes
public int size() {
return bytes.length;
public ZstdByteSize size() {
return new ZstdByteSize(bytes.length);
}

/// Digests this dictionary once for compression at `level`, ready to share by
Expand Down
Loading
Loading