Skip to content

Commit 9a72557

Browse files
committed
feat: replace raw int compression level with ZstdCompressionLevel
Introduces ZstdCompressionLevel, a record wrapping the compression level as a validated int, checked in its compact constructor against Zstd.minCompressionLevel()/maxCompressionLevel() (IllegalArgumentException if out of range) rather than deferring to native clamping/errors. The accepted range is queried from libzstd once and cached, since it never changes for the life of the process. Ships DEFAULT/FASTEST/MAX constants for the common cases; construct directly (new ZstdCompressionLevel(n)) for any other level — there is no separate of(int) factory, since a public record's canonical constructor can't be hidden anyway. Replaces every naked int level parameter with it across Zstd, ZstdCompressContext, ZstdCompressStream, ZstdCompressDictionary, ZstdDictionary, and ZstdOutputStream. Zstd.min/max/defaultCompressionLevel() keep returning int, since they're the bound queries the new type validates against. Breaking change; the library is pre-1.0 so no compatibility shims are added. Closes #93.
1 parent 77bea85 commit 9a72557

28 files changed

Lines changed: 314 additions & 142 deletions

.github/smoke/src/test/java/SmokeTest.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.github.dfa1.zstd.Zstd;
66
import io.github.dfa1.zstd.ZstdCompressContext;
77
import io.github.dfa1.zstd.ZstdCompressDictionary;
8+
import io.github.dfa1.zstd.ZstdCompressionLevel;
89
import io.github.dfa1.zstd.ZstdCompressParameter;
910
import io.github.dfa1.zstd.ZstdCompressStream;
1011
import io.github.dfa1.zstd.ZstdDecompressContext;
@@ -82,9 +83,11 @@ void versionAndSizing() {
8283
check(def >= min && def <= max, "defaultCompressionLevel() out of [min,max]");
8384

8485
check(Zstd.compressBound(1000) >= 1000, "compressBound() below input size");
85-
check(Zstd.estimateCompressContextSize(def) > 0, "estimateCompressContextSize() not positive");
86+
check(Zstd.estimateCompressContextSize(ZstdCompressionLevel.DEFAULT) > 0,
87+
"estimateCompressContextSize() not positive");
8688
check(Zstd.estimateDecompressContextSize() > 0, "estimateDecompressContextSize() not positive");
87-
check(Zstd.estimateCompressDictSize(4096, def) > 0, "estimateCompressDictSize() not positive");
89+
check(Zstd.estimateCompressDictSize(4096, ZstdCompressionLevel.DEFAULT) > 0,
90+
"estimateCompressDictSize() not positive");
8891
check(Zstd.estimateDecompressDictSize(4096) > 0, "estimateDecompressDictSize() not positive");
8992
}
9093

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

98-
byte[] compressed = Zstd.compress(original, Zstd.maxCompressionLevel());
101+
byte[] compressed = Zstd.compress(original, ZstdCompressionLevel.MAX);
99102
checkArrayEquals(original, Zstd.decompress(compressed), "compress(byte[], level) round-trip mismatch");
100103
check(compressed.length < original.length, "expected compression to shrink the input");
101104
}
@@ -199,7 +202,7 @@ void skippableFrames() {
199202
void compressContextAdvancedParameters() {
200203
byte[] original = sampleText();
201204
try (ZstdCompressContext cctx = new ZstdCompressContext()) {
202-
cctx.level(5)
205+
cctx.level(new ZstdCompressionLevel(5))
203206
.checksum(true)
204207
.longDistanceMatching(true)
205208
.windowLog(20)
@@ -211,7 +214,7 @@ void compressContextAdvancedParameters() {
211214
check(cctx.sizeOf() > 0, "cctx.sizeOf() not positive");
212215

213216
cctx.reset(ZstdResetDirective.SESSION_AND_PARAMETERS);
214-
byte[] afterReset = cctx.level(3).compress(original);
217+
byte[] afterReset = cctx.level(new ZstdCompressionLevel(3)).compress(original);
215218
checkArrayEquals(original, Zstd.decompress(afterReset, original.length),
216219
"compress after SESSION_AND_PARAMETERS reset mismatch");
217220
}
@@ -361,7 +364,8 @@ void streamingIo() throws IOException {
361364
}
362365

363366
ByteArrayOutputStream sinkPledged = new ByteArrayOutputStream();
364-
try (ZstdOutputStream zout = ZstdOutputStream.withPledgedSize(sinkPledged, 5, original.length)) {
367+
try (ZstdOutputStream zout =
368+
ZstdOutputStream.withPledgedSize(sinkPledged, new ZstdCompressionLevel(5), original.length)) {
365369
zout.write(original);
366370
}
367371
byte[] pledgedFrame = sinkPledged.toByteArray();

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ git tags, which trigger publication to Maven Central.
66

77
## [Unreleased]
88

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

1124
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ One-shot round-trip with `byte[]` — the convenient path:
2929
import io.github.dfa1.zstd.Zstd;
3030

3131
byte[] data = ...;
32-
byte[] frame = Zstd.compress(data); // or Zstd.compress(data, level)
32+
byte[] frame = Zstd.compress(data); // or Zstd.compress(data, new ZstdCompressionLevel(19))
3333
byte[] back = Zstd.decompress(frame); // size read from the frame header
3434
```
3535

benchmark/src/main/java/io/github/dfa1/zstd/bench/CompressBenchmark.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.airlift.compress.v3.zstd.ZstdJavaCompressor;
66
import io.github.dfa1.zstd.Zstd;
77
import io.github.dfa1.zstd.ZstdCompressContext;
8+
import io.github.dfa1.zstd.ZstdCompressionLevel;
89
import java.lang.foreign.Arena;
910
import java.lang.foreign.MemorySegment;
1011
import java.util.concurrent.TimeUnit;
@@ -58,7 +59,7 @@ public void setup() {
5859
src = BenchData.generate(size);
5960
int bound = (int) Zstd.compressBound(size);
6061

61-
ffmCtx = new ZstdCompressContext().level(level);
62+
ffmCtx = new ZstdCompressContext().level(new ZstdCompressionLevel(level));
6263
ffmDst = new byte[bound];
6364

6465
arena = Arena.ofConfined();

benchmark/src/main/java/io/github/dfa1/zstd/bench/GoldenCorpusBenchmark.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import io.github.dfa1.zstd.Zstd;
66
import io.github.dfa1.zstd.ZstdCompressContext;
7+
import io.github.dfa1.zstd.ZstdCompressionLevel;
78
import io.github.dfa1.zstd.ZstdDecompressContext;
89
import java.io.UncheckedIOException;
910
import java.io.IOException;
@@ -88,7 +89,7 @@ public void setup() {
8889
srcSize = src.length;
8990
frame = Zstd.compress(src);
9091

91-
cctx = new ZstdCompressContext().level(level);
92+
cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(level));
9293
dctx = new ZstdDecompressContext();
9394
bound = (int) Zstd.compressBound(srcSize);
9495
compressDst = new byte[bound];

benchmark/src/main/java/io/github/dfa1/zstd/bench/MultiThreadCompressBenchmark.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import io.github.dfa1.zstd.Zstd;
66
import io.github.dfa1.zstd.ZstdCompressContext;
7+
import io.github.dfa1.zstd.ZstdCompressionLevel;
78
import io.github.dfa1.zstd.ZstdCompressParameter;
89
import java.lang.foreign.Arena;
910
import java.lang.foreign.MemorySegment;
@@ -59,7 +60,7 @@ public void setup() {
5960
byte[] src = BenchData.generate(size);
6061
int bound = (int) Zstd.compressBound(size);
6162

62-
ctx = new ZstdCompressContext().level(level);
63+
ctx = new ZstdCompressContext().level(new ZstdCompressionLevel(level));
6364
if (nbWorkers > 0) {
6465
ctx.parameter(ZstdCompressParameter.NB_WORKERS, nbWorkers);
6566
}

docs/how-to.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Task-focused recipes. Each assumes you have the library on the classpath (see th
88
Reuse a context to amortize native allocation across many calls:
99

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

2828
```java
29-
try (ZstdCompressContext cctx = new ZstdCompressContext().level(19)) {
29+
try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) {
3030
byte[] a = cctx.compress(first);
3131

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

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

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

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

233233
```java
234-
try (var zout = ZstdOutputStream.withPledgedSize(sink, 6, data.length)) {
234+
try (var zout = ZstdOutputStream.withPledgedSize(sink, new ZstdCompressionLevel(6), data.length)) {
235235
zout.write(data); // pledge must equal bytes written
236236
}
237237
MemorySegment src = MemorySegment.ofBuffer(mmap); // downstream, in a mapped reader

docs/tutorial.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ incrementally, so memory stays flat no matter how big the payload.
116116

117117
```java
118118
// compress a file as you write it
119-
try (var out = new ZstdOutputStream(Files.newOutputStream(packed), 9)) {
119+
try (var out = new ZstdOutputStream(Files.newOutputStream(packed), new ZstdCompressionLevel(9))) {
120120
Files.copy(source, out);
121121
}
122122

integration-tests/src/test/java/io/github/dfa1/zstd/it/GoldenCorpusTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.github.dfa1.zstd.ZstdDictionaryId;
44
import io.github.dfa1.zstd.Zstd;
55
import io.github.dfa1.zstd.ZstdCompressContext;
6+
import io.github.dfa1.zstd.ZstdCompressionLevel;
67
import io.github.dfa1.zstd.ZstdDecompressContext;
78
import io.github.dfa1.zstd.ZstdDictionary;
89
import io.github.dfa1.zstd.ZstdException;
@@ -136,7 +137,7 @@ void javaCompressJniDecompress(String name, Path file) {
136137
byte[] data = read(file);
137138

138139
// When
139-
byte[] frame = Zstd.compress(data, Zstd.defaultCompressionLevel());
140+
byte[] frame = Zstd.compress(data, ZstdCompressionLevel.DEFAULT);
140141

141142
// Then
142143
assertThat(com.github.luben.zstd.Zstd.decompress(frame, data.length)).isEqualTo(data);

integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdInteropExtrasTest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.github.luben.zstd.ZstdCompressCtx;
44
import io.github.dfa1.zstd.ZstdDictionaryId;
55
import io.github.dfa1.zstd.Zstd;
6+
import io.github.dfa1.zstd.ZstdCompressionLevel;
67
import io.github.dfa1.zstd.ZstdDictionary;
78
import io.github.dfa1.zstd.ZstdException;
89
import io.github.dfa1.zstd.ZstdFrame;
@@ -127,7 +128,7 @@ void jniStreamSkipsJavaSkippableFrame() {
127128
byte[] payload = "after the skippable frame ".repeat(1000).getBytes(StandardCharsets.UTF_8);
128129
byte[] meta = "sidecar-metadata".getBytes(StandardCharsets.UTF_8);
129130
byte[] skippable = ZstdFrame.writeSkippableFrame(meta, 0);
130-
byte[] real = Zstd.compress(payload, Zstd.defaultCompressionLevel());
131+
byte[] real = Zstd.compress(payload, ZstdCompressionLevel.DEFAULT);
131132

132133
// When
133134
byte[] restored = jniStreamDecode(concat(skippable, real));
@@ -164,8 +165,8 @@ class MultiFrame {
164165
void javaFramesConcatReadByJniStream() {
165166
// Given
166167
byte[] joined = concat(
167-
Zstd.compress(a, Zstd.defaultCompressionLevel()),
168-
Zstd.compress(b, Zstd.defaultCompressionLevel()));
168+
Zstd.compress(a, ZstdCompressionLevel.DEFAULT),
169+
Zstd.compress(b, ZstdCompressionLevel.DEFAULT));
169170

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

268269
// When
269-
try (ZstdOutputStream zout = new ZstdOutputStream(sink, 7)) {
270+
try (ZstdOutputStream zout = new ZstdOutputStream(sink, new ZstdCompressionLevel(7))) {
270271
writeInChunks(zout, data);
271272
}
272273

0 commit comments

Comments
 (0)