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
16 changes: 10 additions & 6 deletions .github/smoke/src/test/java/SmokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdCompressDictionary;
import io.github.dfa1.zstd.ZstdCompressionLevel;
import io.github.dfa1.zstd.ZstdCompressParameter;
import io.github.dfa1.zstd.ZstdCompressStream;
import io.github.dfa1.zstd.ZstdDecompressContext;
Expand Down Expand Up @@ -82,9 +83,11 @@ void versionAndSizing() {
check(def >= min && def <= max, "defaultCompressionLevel() out of [min,max]");

check(Zstd.compressBound(1000) >= 1000, "compressBound() below input size");
check(Zstd.estimateCompressContextSize(def) > 0, "estimateCompressContextSize() not positive");
check(Zstd.estimateCompressContextSize(ZstdCompressionLevel.DEFAULT) > 0,
"estimateCompressContextSize() not positive");
check(Zstd.estimateDecompressContextSize() > 0, "estimateDecompressContextSize() not positive");
check(Zstd.estimateCompressDictSize(4096, def) > 0, "estimateCompressDictSize() not positive");
check(Zstd.estimateCompressDictSize(4096, ZstdCompressionLevel.DEFAULT) > 0,
"estimateCompressDictSize() not positive");
check(Zstd.estimateDecompressDictSize(4096) > 0, "estimateDecompressDictSize() not positive");
}

Expand All @@ -95,7 +98,7 @@ void coreRoundTrip() {
byte[] compressedDefault = Zstd.compress(original);
checkArrayEquals(original, Zstd.decompress(compressedDefault), "compress(byte[]) round-trip mismatch");

byte[] compressed = Zstd.compress(original, Zstd.maxCompressionLevel());
byte[] compressed = Zstd.compress(original, ZstdCompressionLevel.MAX);
checkArrayEquals(original, Zstd.decompress(compressed), "compress(byte[], level) round-trip mismatch");
check(compressed.length < original.length, "expected compression to shrink the input");
}
Expand Down Expand Up @@ -199,7 +202,7 @@ void skippableFrames() {
void compressContextAdvancedParameters() {
byte[] original = sampleText();
try (ZstdCompressContext cctx = new ZstdCompressContext()) {
cctx.level(5)
cctx.level(new ZstdCompressionLevel(5))
.checksum(true)
.longDistanceMatching(true)
.windowLog(20)
Expand All @@ -211,7 +214,7 @@ void compressContextAdvancedParameters() {
check(cctx.sizeOf() > 0, "cctx.sizeOf() not positive");

cctx.reset(ZstdResetDirective.SESSION_AND_PARAMETERS);
byte[] afterReset = cctx.level(3).compress(original);
byte[] afterReset = cctx.level(new ZstdCompressionLevel(3)).compress(original);
checkArrayEquals(original, Zstd.decompress(afterReset, original.length),
"compress after SESSION_AND_PARAMETERS reset mismatch");
}
Expand Down Expand Up @@ -361,7 +364,8 @@ void streamingIo() throws IOException {
}

ByteArrayOutputStream sinkPledged = new ByteArrayOutputStream();
try (ZstdOutputStream zout = ZstdOutputStream.withPledgedSize(sinkPledged, 5, original.length)) {
try (ZstdOutputStream zout =
ZstdOutputStream.withPledgedSize(sinkPledged, new ZstdCompressionLevel(5), original.length)) {
zout.write(original);
}
byte[] pledgedFrame = sinkPledged.toByteArray();
Expand Down
13 changes: 13 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]

### Changed
- **Breaking:** every public API that took a raw `int` compression level now
takes a `ZstdCompressionLevel` value type, which validates the level against
the linked libzstd's accepted range at construction (throwing
`IllegalArgumentException`) rather than deferring to native clamping/errors.
Affects `Zstd.compress(byte[], …)`, `Zstd.estimateCompressContextSize`,
`Zstd.estimateCompressDictSize`, `ZstdCompressContext.level`,
`ZstdCompressStream`, `ZstdOutputStream`, `ZstdCompressDictionary`,
`ZstdDictionary.compressDict`/`trainCover`/`trainFastCover`/`finalizeFrom`.
Use `new ZstdCompressionLevel(19)` or the `DEFAULT`/`FASTEST`/`MAX` constants.
The `Zstd.min/max/defaultCompressionLevel()` bound queries still return `int`.
([#93](https://github.com/dfa1/zstd-java/issues/93))

## [0.10] - 2026-07-18

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ One-shot round-trip with `byte[]` — the convenient path:
import io.github.dfa1.zstd.Zstd;

byte[] data = ...;
byte[] frame = Zstd.compress(data); // or Zstd.compress(data, level)
byte[] frame = Zstd.compress(data); // or Zstd.compress(data, new ZstdCompressionLevel(19))
byte[] back = Zstd.decompress(frame); // size read from the frame header
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.airlift.compress.v3.zstd.ZstdJavaCompressor;
import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdCompressionLevel;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -58,7 +59,7 @@ public void setup() {
src = BenchData.generate(size);
int bound = (int) Zstd.compressBound(size);

ffmCtx = new ZstdCompressContext().level(level);
ffmCtx = new ZstdCompressContext().level(new ZstdCompressionLevel(level));
ffmDst = new byte[bound];

arena = Arena.ofConfined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdCompressionLevel;
import io.github.dfa1.zstd.ZstdDecompressContext;
import java.io.UncheckedIOException;
import java.io.IOException;
Expand Down Expand Up @@ -88,7 +89,7 @@ public void setup() {
srcSize = src.length;
frame = Zstd.compress(src);

cctx = new ZstdCompressContext().level(level);
cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(level));
dctx = new ZstdDecompressContext();
bound = (int) Zstd.compressBound(srcSize);
compressDst = new byte[bound];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdCompressionLevel;
import io.github.dfa1.zstd.ZstdCompressParameter;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
Expand Down Expand Up @@ -59,7 +60,7 @@ public void setup() {
byte[] src = BenchData.generate(size);
int bound = (int) Zstd.compressBound(size);

ctx = new ZstdCompressContext().level(level);
ctx = new ZstdCompressContext().level(new ZstdCompressionLevel(level));
if (nbWorkers > 0) {
ctx.parameter(ZstdCompressParameter.NB_WORKERS, nbWorkers);
}
Expand Down
12 changes: 6 additions & 6 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Task-focused recipes. Each assumes you have the library on the classpath (see th
Reuse a context to amortize native allocation across many calls:

```java
try (ZstdCompressContext cctx = new ZstdCompressContext().level(19);
try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19));
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
byte[] packed = cctx.compress(message);
byte[] restored = dctx.decompress(packed, message.length);
Expand All @@ -26,7 +26,7 @@ or to abort a half-written frame and start clean — without freeing and recreat
it. Pick what to clear with `ZstdResetDirective`:

```java
try (ZstdCompressContext cctx = new ZstdCompressContext().level(19)) {
try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) {
byte[] a = cctx.compress(first);

// Cheap: drop any unflushed frame state, keep the level and parameters.
Expand All @@ -51,7 +51,7 @@ matching) set on the context. To combine the two, make the dictionary *sticky*
with `loadDictionary` — then the normal `compress` path honors both:

```java
try (ZstdCompressContext cctx = new ZstdCompressContext().level(19).checksum(true)) {
try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19)).checksum(true)) {
cctx.loadDictionary(dict); // ZstdDictionary, or a native MemorySegment
byte[] frame = cctx.compress(record); // dictionary + checksum, together
}
Expand All @@ -62,7 +62,7 @@ by reference — no per-call digesting, no copy. It pairs with `reset` for a
pooled, recycled context:

```java
try (ZstdCompressDictionary cdict = dict.compressDict(19)) {
try (ZstdCompressDictionary cdict = dict.compressDict(new ZstdCompressionLevel(19))) {
// one cctx per pooled worker, all sharing the one digested dictionary
try (ZstdCompressContext cctx = new ZstdCompressContext()) {
cctx.refDictionary(cdict); // borrowed; cdict must outlive cctx
Expand Down Expand Up @@ -105,7 +105,7 @@ ZstdDictionary reloaded = ZstdDictionary.of(persisted);
On a hot path, digest the dictionary once to skip per-call setup:

```java
try (ZstdCompressDictionary cdict = dict.compressDict(19);
try (ZstdCompressDictionary cdict = dict.compressDict(new ZstdCompressionLevel(19));
ZstdDecompressDictionary ddict = dict.decompressDict();
ZstdCompressContext cctx = new ZstdCompressContext();
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
Expand Down Expand Up @@ -231,7 +231,7 @@ can't size the arena (see [the explanation](zero-copy.md)). Tell the encoder the
total up front and it stamps the content size into the header:

```java
try (var zout = ZstdOutputStream.withPledgedSize(sink, 6, data.length)) {
try (var zout = ZstdOutputStream.withPledgedSize(sink, new ZstdCompressionLevel(6), data.length)) {
zout.write(data); // pledge must equal bytes written
}
MemorySegment src = MemorySegment.ofBuffer(mmap); // downstream, in a mapped reader
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ incrementally, so memory stays flat no matter how big the payload.

```java
// compress a file as you write it
try (var out = new ZstdOutputStream(Files.newOutputStream(packed), 9)) {
try (var out = new ZstdOutputStream(Files.newOutputStream(packed), new ZstdCompressionLevel(9))) {
Files.copy(source, out);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.dfa1.zstd.ZstdDictionaryId;
import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdCompressionLevel;
import io.github.dfa1.zstd.ZstdDecompressContext;
import io.github.dfa1.zstd.ZstdDictionary;
import io.github.dfa1.zstd.ZstdException;
Expand Down Expand Up @@ -136,7 +137,7 @@ void javaCompressJniDecompress(String name, Path file) {
byte[] data = read(file);

// When
byte[] frame = Zstd.compress(data, Zstd.defaultCompressionLevel());
byte[] frame = Zstd.compress(data, ZstdCompressionLevel.DEFAULT);

// Then
assertThat(com.github.luben.zstd.Zstd.decompress(frame, data.length)).isEqualTo(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.github.luben.zstd.ZstdCompressCtx;
import io.github.dfa1.zstd.ZstdDictionaryId;
import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressionLevel;
import io.github.dfa1.zstd.ZstdDictionary;
import io.github.dfa1.zstd.ZstdException;
import io.github.dfa1.zstd.ZstdFrame;
Expand Down Expand Up @@ -127,7 +128,7 @@ void jniStreamSkipsJavaSkippableFrame() {
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[] real = Zstd.compress(payload, Zstd.defaultCompressionLevel());
byte[] real = Zstd.compress(payload, ZstdCompressionLevel.DEFAULT);

// When
byte[] restored = jniStreamDecode(concat(skippable, real));
Expand Down Expand Up @@ -164,8 +165,8 @@ class MultiFrame {
void javaFramesConcatReadByJniStream() {
// Given
byte[] joined = concat(
Zstd.compress(a, Zstd.defaultCompressionLevel()),
Zstd.compress(b, Zstd.defaultCompressionLevel()));
Zstd.compress(a, ZstdCompressionLevel.DEFAULT),
Zstd.compress(b, ZstdCompressionLevel.DEFAULT));

// When
byte[] restored = jniStreamDecode(joined);
Expand Down Expand Up @@ -266,7 +267,7 @@ void javaChunkedWriteJniRead(String name, byte[] data) throws IOException {
ByteArrayOutputStream sink = new ByteArrayOutputStream();

// When
try (ZstdOutputStream zout = new ZstdOutputStream(sink, 7)) {
try (ZstdOutputStream zout = new ZstdOutputStream(sink, new ZstdCompressionLevel(7))) {
writeInChunks(zout, data);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdCompressDictionary;
import io.github.dfa1.zstd.ZstdCompressionLevel;
import io.github.dfa1.zstd.ZstdCompressParameter;
import io.github.dfa1.zstd.ZstdDecompressContext;
import io.github.dfa1.zstd.ZstdDictionary;
Expand Down Expand Up @@ -59,7 +60,7 @@ void jniCompressJavaDecompress(int level, byte[] data) {
@ParameterizedTest
@MethodSource("payloadsAndLevels")
void javaCompressJniDecompress(int level, byte[] data) {
byte[] frame = Zstd.compress(data, level);
byte[] frame = Zstd.compress(data, new ZstdCompressionLevel(level));
assertThat(com.github.luben.zstd.Zstd.decompress(frame, data.length)).isEqualTo(data);
}

Expand All @@ -71,7 +72,7 @@ void javaStreamToJniStream() throws Exception {
byte[] data = "interop streaming ".repeat(50_000).getBytes(StandardCharsets.UTF_8);

ByteArrayOutputStream sink = new ByteArrayOutputStream();
try (ZstdOutputStream zout = new ZstdOutputStream(sink, 7)) {
try (ZstdOutputStream zout = new ZstdOutputStream(sink, new ZstdCompressionLevel(7))) {
zout.write(data);
}
byte[] restored;
Expand Down Expand Up @@ -197,7 +198,7 @@ void javaReferencedDigestedDictJniDictDecompress() {
byte[] sample = sample(44);

byte[] frame;
try (ZstdCompressDictionary cdict = new ZstdCompressDictionary(dict, Zstd.defaultCompressionLevel());
try (ZstdCompressDictionary cdict = new ZstdCompressDictionary(dict, ZstdCompressionLevel.DEFAULT);
ZstdCompressContext ctx = new ZstdCompressContext()) {
ctx.refDictionary(cdict);
frame = ctx.compress(sample);
Expand Down
21 changes: 10 additions & 11 deletions zstd/src/main/java/io/github/dfa1/zstd/Zstd.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,22 @@ public final class Zstd {
/// @param src bytes to compress
/// @return a self-describing zstd frame
public static byte[] compress(byte[] src) {
return compress(src, defaultCompressionLevel());
return compress(src, ZstdCompressionLevel.DEFAULT);
}

/// Compresses `src` at the given level.
///
/// @param src bytes to compress
/// @param level compression level in [[#minCompressionLevel()], [#maxCompressionLevel()]];
/// higher is smaller but slower
/// @param level the compression level to use; higher is smaller but slower
/// @return a self-describing zstd frame
public static byte[] compress(byte[] src, int level) {
public static byte[] compress(byte[] src, ZstdCompressionLevel level) {
Objects.requireNonNull(src, "src");
try (Arena arena = Arena.ofConfined()) {
MemorySegment in = copyIn(arena, src);
long bound = compressBound(src.length);
MemorySegment out = arena.allocate(bound);
long written = NativeCall.checkReturnValue(() -> (long) Bindings.COMPRESS.invokeExact(
out, bound, in, (long) src.length, level));
out, bound, in, (long) src.length, level.value()));
return copyOut(out, written);
}
}
Expand Down Expand Up @@ -239,11 +238,11 @@ public static int defaultCompressionLevel() {
/// Estimates the memory a compression context will use at `level`, before
/// creating one — useful for budgeting.
///
/// @param level the compression level
/// @param level the compression level to use
/// @return the estimated context size in bytes
public static long estimateCompressContextSize(int level) {
public static long estimateCompressContextSize(ZstdCompressionLevel level) {
try {
return (long) Bindings.ESTIMATE_CCTX_SIZE.invokeExact(level);
return (long) Bindings.ESTIMATE_CCTX_SIZE.invokeExact(level.value());
} catch (Throwable t) {
throw NativeCall.rethrow(t);
}
Expand All @@ -264,11 +263,11 @@ public static long estimateDecompressContextSize() {
/// will use at `level`.
///
/// @param dictSize the raw dictionary size in bytes
/// @param level the compression level
/// @param level the compression level to use
/// @return the estimated digested-dictionary size in bytes
public static long estimateCompressDictSize(long dictSize, int level) {
public static long estimateCompressDictSize(long dictSize, ZstdCompressionLevel level) {
try {
return (long) Bindings.ESTIMATE_CDICT_SIZE.invokeExact(dictSize, level);
return (long) Bindings.ESTIMATE_CDICT_SIZE.invokeExact(dictSize, level.value());
} catch (Throwable t) {
throw NativeCall.rethrow(t);
}
Expand Down
Loading
Loading