Skip to content

Commit ec322c5

Browse files
dfa1claude
andcommitted
feat: advanced compression parameters (checksum, LDM, windowLog)
ZstdCompressCtx now drives ZSTD_compress2 so the advanced parameters set via ZSTD_CCtx_setParameter take effect: - ZstdCompressParameter enum mirrors ZSTD_cParameter (values from the manual) - ctx.parameter(p, v) generic setter (natively validated -> ZstdException on out-of-range), plus checksum(bool), longDistanceMatching(bool), windowLog(int) - level() and the byte[]/MemorySegment compress paths route through compress2; dictionary overloads keep the legacy level-arg path - nbWorkers is in the enum but a no-op on the bundled single-threaded build - bind ZSTD_compress2; compatibility.md now 37/186 Tests: checksum adds its 4-byte trailer and round-trips; a corrupted checksummed frame is rejected; LDM/windowLog round-trip; generic level matches level(); out-of-range value throws. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 68bc1dd commit ec322c5

5 files changed

Lines changed: 218 additions & 10 deletions

File tree

docs/compatibility.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ and semantics follow the [official manual](https://facebook.github.io/zstd/doc/a
55

66
- zstd version: **1.6.0** (vendored `third_party/zstd`)
77
- Public symbols exported by `libzstd`: **186**
8-
- Bound so far: **36** (~19%)
8+
- Bound so far: **37** (~20%)
99

1010
"Bound" means the symbol has a `MethodHandle` in `Bindings` and is reachable
1111
through the public Java API. The rest are reachable from native code but not yet
@@ -27,7 +27,7 @@ low-level variants that an idiomatic Java API does not need.
2727
| Dictionary training (ZDICT) | 4 / 12 | `trainFromBuffer`; cover/fastCover optimizers not bound |
2828
| Streaming — compress | 3 / 22 | `ZstdOutputStream` (compressStream2 + buffer sizes) |
2929
| Streaming — decompress | 3 / 15 | `ZstdInputStream` (decompressStream + buffer sizes) |
30-
| Advanced parameters | 1 / 38 | only `CCtx_setParameter` (level), used by the streams; bounds/MT/LDM not bound |
30+
| Advanced parameters | 2 / 38 | `CCtx_setParameter` + `compress2` via `ZstdCompressCtx` (level, checksum, LDM, windowLog…); MT/getBounds not bound |
3131
| Frame inspection | 1 / 13 | only `getFrameContentSize`; header/skippable/dictID-from-frame not bound |
3232
| Memory sizing | 0 / 14 | `sizeof_*` / `estimate*` accounting not bound |
3333
| Low-level block | 0 / 12 | expert block/continue API not bound |
@@ -54,11 +54,12 @@ low-level variants that an idiomatic Java API does not need.
5454
| `ZDICT_isError`, `ZDICT_getErrorName` | internal error mapping in `ZstdDictionary` |
5555
| `ZSTD_compressStream2`, `ZSTD_CStreamInSize`, `ZSTD_CStreamOutSize`, `ZSTD_CCtx_setParameter` | `ZstdOutputStream` |
5656
| `ZSTD_decompressStream`, `ZSTD_DStreamInSize`, `ZSTD_DStreamOutSize` | `ZstdInputStream` |
57+
| `ZSTD_compress2`, `ZSTD_CCtx_setParameter` | `ZstdCompressCtx.parameter` / `checksum` / `longDistanceMatching` / `windowLog` (+ `ZstdCompressParameter`) |
5758

5859
## Roadmap (priority order)
5960

6061
1. ~~**Streaming**~~ — done: `ZstdOutputStream` / `ZstdInputStream` (`compressStream2`, `decompressStream`, bounded buffers). Remaining: dictionary-on-stream, `MemorySegment`-buffer driver, `pledgedSrcSize`.
61-
2. **Advanced parameters**`ZSTD_CCtx_setParameter` (level is bound; expose the rest) + `cParam_getBounds`: checksums, `nbWorkers`, long-distance matching, window log, `pledgedSrcSize`.
62+
2. **Advanced parameters**done for compression: `CCtx_setParameter` + `compress2` via `ZstdCompressCtx` (`checksum`, `longDistanceMatching`, `windowLog`, generic `parameter`). Remaining: `cParam_getBounds`, `pledgedSrcSize`, and `nbWorkers` (needs a multithreaded native build).
6263
3. **Frame inspection**`getFrameHeader`, `isFrame`, `findFrameCompressedSize`, `decompressBound`, `getDictID_from*`, skippable frames.
6364
4. **Better dictionaries**`ZDICT_optimizeTrainFromBuffer_cover` / `_fastCover`, `finalizeDictionary`.
6465
5. **Typed errors**`ZSTD_getErrorCode` / `ZSTD_getErrorString`.
@@ -200,7 +201,7 @@ low-level variants that an idiomatic Java API does not need.
200201
| `ZSTD_resetDStream` ||
201202
| `ZSTD_sizeof_DStream` ||
202203

203-
### Advanced parameters (1/38)
204+
### Advanced parameters (2/38)
204205

205206
| Symbol | Bound |
206207
|---|:---:|
@@ -237,7 +238,7 @@ low-level variants that an idiomatic Java API does not need.
237238
| `ZSTD_DCtx_setMaxWindowSize` ||
238239
| `ZSTD_DCtx_setParameter` ||
239240
| `ZSTD_cParam_getBounds` ||
240-
| `ZSTD_compress2` | |
241+
| `ZSTD_compress2` | |
241242
| `ZSTD_createCCtxParams` ||
242243
| `ZSTD_dParam_getBounds` ||
243244
| `ZSTD_estimateCCtxSize_usingCCtxParams` ||

zstd/src/main/java/io/github/dfa1/zstdffm/Bindings.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ final class Bindings {
9191
NativeLibrary.lookup("ZSTD_CCtx_setParameter",
9292
FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_INT, JAVA_INT));
9393

94+
// size_t ZSTD_compress2(ZSTD_CCtx*, void* dst, size_t dstCap, const void* src, size_t srcSize)
95+
// Uses the advanced parameters set on the context (unlike ZSTD_compressCCtx).
96+
static final MethodHandle COMPRESS2 =
97+
NativeLibrary.lookup("ZSTD_compress2",
98+
FunctionDescriptor.of(JAVA_LONG, ADDRESS, ADDRESS, JAVA_LONG, ADDRESS, JAVA_LONG));
99+
94100
// size_t ZSTD_compressStream2(ZSTD_CCtx*, ZSTD_outBuffer*, ZSTD_inBuffer*, ZSTD_EndDirective)
95101
static final MethodHandle COMPRESS_STREAM2 =
96102
NativeLibrary.lookup("ZSTD_compressStream2",

zstd/src/main/java/io/github/dfa1/zstdffm/ZstdCompressCtx.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,54 @@ private static MemorySegment create() {
4343
/// @return `this`, for chaining
4444
public ZstdCompressCtx level(int level) {
4545
this.level = level;
46+
setParam(ZstdCompressParameter.COMPRESSION_LEVEL, level);
4647
return this;
4748
}
4849

49-
/// Compresses `src` into a new zstd frame using this context.
50+
/// Sets an advanced compression parameter for subsequent
51+
/// {@link #compress(byte[])} / {@link #compress(MemorySegment, MemorySegment)}
52+
/// calls. The setting is sticky across calls until changed.
53+
///
54+
/// Advanced parameters do not apply to the dictionary `compress` overloads.
55+
///
56+
/// @param parameter the parameter to set
57+
/// @param value the value, validated natively against the parameter's bounds
58+
/// @return `this`, for chaining
59+
/// @throws ZstdException if the value is out of range for the parameter
60+
public ZstdCompressCtx parameter(ZstdCompressParameter parameter, int value) {
61+
setParam(parameter, value);
62+
return this;
63+
}
64+
65+
/// Appends a 32-bit content checksum to each frame, which decompression
66+
/// verifies and rejects on mismatch. Off by default.
67+
///
68+
/// @param enabled whether to write a checksum
69+
/// @return `this`, for chaining
70+
public ZstdCompressCtx checksum(boolean enabled) {
71+
return parameter(ZstdCompressParameter.CHECKSUM_FLAG, enabled ? 1 : 0);
72+
}
73+
74+
/// Enables long-distance matching for a better ratio on large, repetitive inputs.
75+
///
76+
/// @param enabled whether to enable long-distance matching
77+
/// @return `this`, for chaining
78+
public ZstdCompressCtx longDistanceMatching(boolean enabled) {
79+
return parameter(ZstdCompressParameter.ENABLE_LONG_DISTANCE_MATCHING, enabled ? 1 : 0);
80+
}
81+
82+
/// Sets the maximum back-reference distance as a power of two (larger window =
83+
/// better ratio, more memory). Decompression of large windows may require
84+
/// raising the decompressor's window limit.
85+
///
86+
/// @param windowLog the base-2 log of the window size
87+
/// @return `this`, for chaining
88+
public ZstdCompressCtx windowLog(int windowLog) {
89+
return parameter(ZstdCompressParameter.WINDOW_LOG, windowLog);
90+
}
91+
92+
/// Compresses `src` into a new zstd frame using this context and its
93+
/// advanced parameters.
5094
///
5195
/// @param src the bytes to compress
5296
/// @return a self-describing zstd frame
@@ -55,12 +99,16 @@ public byte[] compress(byte[] src) {
5599
MemorySegment in = Zstd.copyIn(arena, src);
56100
long bound = Zstd.compressBound(src.length);
57101
MemorySegment out = arena.allocate(bound);
58-
long written = Zstd.call(() -> (long) Bindings.COMPRESS_CCTX.invokeExact(
59-
ptr(), out, bound, in, (long) src.length, level));
102+
long written = Zstd.call(() -> (long) Bindings.COMPRESS2.invokeExact(
103+
ptr(), out, bound, in, (long) src.length));
60104
return Zstd.copyOut(out, written);
61105
}
62106
}
63107

108+
private void setParam(ZstdCompressParameter parameter, int value) {
109+
Zstd.call(() -> (long) Bindings.CCTX_SET_PARAMETER.invokeExact(ptr(), parameter.value(), value));
110+
}
111+
64112
/// Compresses `src` against `dict` at this context's level.
65113
///
66114
/// The dictionary is re-digested on every call; for repeated compression
@@ -114,8 +162,8 @@ public byte[] compress(byte[] src, ZstdCompressDict dict) {
114162
/// @return the number of bytes written into `dst`
115163
/// @throws ZstdException if `dst` is too small or compression fails
116164
public long compress(MemorySegment dst, MemorySegment src) {
117-
return Zstd.call(() -> (long) Bindings.COMPRESS_CCTX.invokeExact(
118-
ptr(), dst, dst.byteSize(), src, src.byteSize(), level));
165+
return Zstd.call(() -> (long) Bindings.COMPRESS2.invokeExact(
166+
ptr(), dst, dst.byteSize(), src, src.byteSize()));
119167
}
120168

121169
/// Zero-copy compression against a pre-digested `dict`, segment to segment.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.github.dfa1.zstdffm;
2+
3+
/// Advanced compression parameters settable on a [ZstdCompressCtx], mirroring
4+
/// `ZSTD_cParameter` from the
5+
/// [official manual](https://facebook.github.io/zstd/doc/api_manual_latest.html).
6+
///
7+
/// Set them with [ZstdCompressCtx#parameter(ZstdCompressParameter, int)]; the
8+
/// context applies them on its next [ZstdCompressCtx#compress(byte[])]. Values
9+
/// are validated natively — an out-of-range value raises a [ZstdException].
10+
public enum ZstdCompressParameter {
11+
12+
/// Compression level. Prefer [ZstdCompressCtx#level(int)].
13+
COMPRESSION_LEVEL(100),
14+
/// Maximum back-reference distance, as a power of two (larger = better ratio, more memory).
15+
WINDOW_LOG(101),
16+
/// Size of the initial probe table, as a power of two.
17+
HASH_LOG(102),
18+
/// Size of the multi-probe search table, as a power of two.
19+
CHAIN_LOG(103),
20+
/// Number of search attempts, as a power of two.
21+
SEARCH_LOG(104),
22+
/// Minimum match length, in bytes.
23+
MIN_MATCH(105),
24+
/// Target match length; impact depends on the strategy.
25+
TARGET_LENGTH(106),
26+
/// Compression strategy (1..9: fast … btultra2).
27+
STRATEGY(107),
28+
/// Enable long-distance matching (1) for better ratio on large inputs.
29+
ENABLE_LONG_DISTANCE_MATCHING(160),
30+
/// Write the decompressed size into the frame header (1, the default) or not (0).
31+
CONTENT_SIZE_FLAG(200),
32+
/// Append a 32-bit content checksum to the frame (1) for integrity, or not (0, the default).
33+
CHECKSUM_FLAG(201),
34+
/// Write the dictionary id into the frame header (1, the default) or not (0).
35+
DICT_ID_FLAG(202),
36+
/// Number of worker threads (0 = single-threaded). Requires a multithreaded
37+
/// build of the native library; the bundled library is single-threaded, so a
38+
/// non-zero value has no effect.
39+
NB_WORKERS(400);
40+
41+
private final int value;
42+
43+
ZstdCompressParameter(int value) {
44+
this.value = value;
45+
}
46+
47+
/// The native `ZSTD_cParameter` integer value.
48+
///
49+
/// @return the enum value as defined by zstd
50+
int value() {
51+
return value;
52+
}
53+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package io.github.dfa1.zstdffm;
2+
3+
import org.junit.jupiter.api.Nested;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.nio.charset.StandardCharsets;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
class ZstdParameterTest {
12+
13+
private static final byte[] PAYLOAD =
14+
"advanced-parameter payload ".repeat(2000).getBytes(StandardCharsets.UTF_8);
15+
16+
@Nested
17+
class Checksum {
18+
19+
@Test
20+
void addsFourByteTrailerAndStillRoundTrips() {
21+
// Given the same input compressed at the same level with and without a checksum
22+
byte[] plain;
23+
byte[] checksummed;
24+
try (ZstdCompressCtx noSum = new ZstdCompressCtx().level(9);
25+
ZstdCompressCtx withSum = new ZstdCompressCtx().level(9).checksum(true)) {
26+
plain = noSum.compress(PAYLOAD);
27+
checksummed = withSum.compress(PAYLOAD);
28+
}
29+
30+
// Then the checksum adds exactly its 4-byte trailer, and the frame still decodes
31+
assertThat(checksummed.length).isEqualTo(plain.length + 4);
32+
assertThat(Zstd.decompress(checksummed)).isEqualTo(PAYLOAD);
33+
}
34+
35+
@Test
36+
void rejectsCorruptedChecksummedFrame() {
37+
// Given a checksummed frame with one body byte flipped
38+
byte[] frame;
39+
try (ZstdCompressCtx ctx = new ZstdCompressCtx().checksum(true)) {
40+
frame = ctx.compress(PAYLOAD);
41+
}
42+
frame[frame.length / 2] ^= 0x01;
43+
44+
// When decompressed / Then the integrity check fails
45+
assertThatThrownBy(() -> Zstd.decompress(frame, PAYLOAD.length))
46+
.isInstanceOf(ZstdException.class);
47+
}
48+
}
49+
50+
@Nested
51+
class Ratio {
52+
53+
@Test
54+
void longDistanceMatchingRoundTrips() {
55+
byte[] frame;
56+
try (ZstdCompressCtx ctx = new ZstdCompressCtx().level(3).longDistanceMatching(true)) {
57+
frame = ctx.compress(PAYLOAD);
58+
}
59+
assertThat(Zstd.decompress(frame)).isEqualTo(PAYLOAD);
60+
}
61+
62+
@Test
63+
void explicitWindowLogRoundTrips() {
64+
byte[] frame;
65+
try (ZstdCompressCtx ctx = new ZstdCompressCtx().windowLog(24)) {
66+
frame = ctx.compress(PAYLOAD);
67+
}
68+
assertThat(Zstd.decompress(frame)).isEqualTo(PAYLOAD);
69+
}
70+
}
71+
72+
@Nested
73+
class GenericSetter {
74+
75+
@Test
76+
void levelViaParameterMatchesLevelMethod() {
77+
// Given the level set generically vs via level()
78+
byte[] viaParam;
79+
byte[] viaMethod;
80+
try (ZstdCompressCtx a = new ZstdCompressCtx().parameter(ZstdCompressParameter.COMPRESSION_LEVEL, 17);
81+
ZstdCompressCtx b = new ZstdCompressCtx().level(17)) {
82+
viaParam = a.compress(PAYLOAD);
83+
viaMethod = b.compress(PAYLOAD);
84+
}
85+
86+
// Then both produce the same frame
87+
assertThat(viaParam).isEqualTo(viaMethod);
88+
}
89+
90+
@Test
91+
void rejectsOutOfRangeValue() {
92+
// Given an absurd window log
93+
try (ZstdCompressCtx ctx = new ZstdCompressCtx()) {
94+
// When set / Then it is rejected natively
95+
assertThatThrownBy(() -> ctx.parameter(ZstdCompressParameter.WINDOW_LOG, 99))
96+
.isInstanceOf(ZstdException.class);
97+
}
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)