Skip to content

Commit 1352e63

Browse files
dfa1claude
andcommitted
feat: typed errors — ZstdException.code()
Callers can branch on the kind of failure instead of matching message text. - ZstdErrorCode enum mirrors ZSTD_ErrorCode; values read from the vendored zstd_errors.h (zstd calls these unstable across versions, but the source is pinned, so they are stable for a given release), with UNKNOWN for unmapped - ZstdException gains code(); Zstd.call attaches it via ZSTD_getErrorCode - bind ZSTD_getErrorCode; compatibility.md now 44/186 Tests: DST_SIZE_TOO_SMALL on undersized buffer; PARAMETER_OUT_OF_BOUND on a bad windowLog; a real native category (not UNKNOWN) for garbage input; enum of() maps known values and falls back to UNKNOWN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 53aced5 commit 1352e63

6 files changed

Lines changed: 209 additions & 6 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: **43** (~23%)
8+
- Bound so far: **44** (~24%)
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
@@ -21,7 +21,7 @@ low-level variants that an idiomatic Java API does not need.
2121
|---|:---:|---|
2222
| Core one-shot | 6 / 7 | compress/decompress/bound/levels — complete for practical use |
2323
| Version | 2 / 2 | complete |
24-
| Errors | 2 / 4 | name-based; typed `getErrorCode`/`getErrorString` not bound |
24+
| Errors | 3 / 4 | name + typed `ZstdErrorCode` (via `getErrorCode`); `getErrorString` not bound |
2525
| Reusable contexts | 6 / 8 | CCtx/DCtx create/free/compress/decompress |
2626
| Dictionary — simple | 8 / 23 | raw + digested (CDict/DDict); `_advanced`/`_byReference` variants not bound |
2727
| Dictionary training (ZDICT) | 4 / 12 | `trainFromBuffer`; cover/fastCover optimizers not bound |
@@ -57,14 +57,15 @@ low-level variants that an idiomatic Java API does not need.
5757
| `ZSTD_compress2`, `ZSTD_CCtx_setParameter` | `ZstdCompressCtx.parameter` / `checksum` / `longDistanceMatching` / `windowLog` (+ `ZstdCompressParameter`) |
5858
| `ZSTD_CCtx_loadDictionary`, `ZSTD_DCtx_loadDictionary` | `ZstdOutputStream` / `ZstdInputStream` dictionary constructors |
5959
| `ZSTD_isFrame`, `ZSTD_findFrameCompressedSize`, `ZSTD_decompressBound`, `ZSTD_getDictID_fromFrame` | `ZstdFrame` |
60+
| `ZSTD_getErrorCode` | `ZstdException.code()` (+ `ZstdErrorCode`) |
6061

6162
## Roadmap (priority order)
6263

6364
1. ~~**Streaming**~~ — done: `ZstdOutputStream` / `ZstdInputStream` (`compressStream2`, `decompressStream`, bounded buffers, dictionary constructors). Remaining: `MemorySegment`-buffer driver, `pledgedSrcSize`.
6465
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).
6566
3. **Frame inspection** — done: `ZstdFrame` (`isFrame`, `compressedSize`, `decompressedBound`, `dictId`). Remaining: `getFrameHeader` (struct out-param), skippable frames, `getDictID_fromDict/CDict/DDict`.
6667
4. **Better dictionaries**`ZDICT_optimizeTrainFromBuffer_cover` / `_fastCover`, `finalizeDictionary`.
67-
5. **Typed errors**`ZSTD_getErrorCode` / `ZSTD_getErrorString`.
68+
5. ~~**Typed errors**~~done: `ZstdException.code()` returns `ZstdErrorCode` (via `getErrorCode`).
6869

6970
## Full symbol table
7071

@@ -89,13 +90,13 @@ low-level variants that an idiomatic Java API does not need.
8990
| `ZSTD_versionNumber` ||
9091
| `ZSTD_versionString` ||
9192

92-
### Errors (2/4)
93+
### Errors (3/4)
9394

9495
| Symbol | Bound |
9596
|---|:---:|
9697
| `ZSTD_getErrorName` ||
9798
| `ZSTD_isError` ||
98-
| `ZSTD_getErrorCode` | |
99+
| `ZSTD_getErrorCode` | |
99100
| `ZSTD_getErrorString` ||
100101

101102
### Reusable contexts (6/8)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ final class Bindings {
6060
static final MethodHandle GET_ERROR_NAME =
6161
NativeLibrary.lookup("ZSTD_getErrorName", FunctionDescriptor.of(ADDRESS, JAVA_LONG));
6262

63+
// ZSTD_ErrorCode ZSTD_getErrorCode(size_t functionResult)
64+
static final MethodHandle GET_ERROR_CODE =
65+
NativeLibrary.lookup("ZSTD_getErrorCode", FunctionDescriptor.of(JAVA_INT, JAVA_LONG));
66+
6367
// int ZSTD_maxCLevel(void) / ZSTD_minCLevel(void) / ZSTD_defaultCLevel(void)
6468
static final MethodHandle MAX_C_LEVEL =
6569
NativeLibrary.lookup("ZSTD_maxCLevel", FunctionDescriptor.of(JAVA_INT));

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ static long call(SizeCall c) {
193193
throw sneaky(t);
194194
}
195195
if (isError(code)) {
196-
throw new ZstdException(errorName(code));
196+
throw new ZstdException(errorName(code), ZstdErrorCode.of(errorCode(code)));
197197
}
198198
return code;
199199
}
@@ -206,6 +206,14 @@ private static boolean isError(long code) {
206206
}
207207
}
208208

209+
private static int errorCode(long code) {
210+
try {
211+
return (int) Bindings.GET_ERROR_CODE.invokeExact(code);
212+
} catch (Throwable t) {
213+
throw sneaky(t);
214+
}
215+
}
216+
209217
@SuppressWarnings("restricted") // reinterpret needed to read a C string of unknown length
210218
private static String errorName(long code) {
211219
try {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package io.github.dfa1.zstdffm;
2+
3+
/// Categories of zstd error, exposed by [ZstdException#code()] so callers can
4+
/// branch on the kind of failure (e.g. distinguish [#CHECKSUM_WRONG] from
5+
/// [#DST_SIZE_TOO_SMALL]) instead of matching on message text.
6+
///
7+
/// The numeric values mirror `ZSTD_ErrorCode` in the vendored zstd header. zstd
8+
/// does not guarantee these values across versions, but this library pins a
9+
/// specific zstd source, so they are stable for a given release. An unrecognised
10+
/// code maps to [#UNKNOWN].
11+
public enum ZstdErrorCode {
12+
13+
/// Not from a zstd error code (e.g. a NULL context or a training failure).
14+
UNKNOWN(-1),
15+
/// No error.
16+
NO_ERROR(0),
17+
/// Unspecified error.
18+
GENERIC(1),
19+
/// Input does not start with a recognised zstd frame prefix.
20+
PREFIX_UNKNOWN(10),
21+
/// Frame requires an unsupported zstd version.
22+
VERSION_UNSUPPORTED(12),
23+
/// Frame uses an unsupported parameter.
24+
FRAME_PARAMETER_UNSUPPORTED(14),
25+
/// Frame's window size exceeds the decoder's limit.
26+
FRAME_PARAMETER_WINDOW_TOO_LARGE(16),
27+
/// Compressed data is corrupt.
28+
CORRUPTION_DETECTED(20),
29+
/// Frame's content checksum did not match.
30+
CHECKSUM_WRONG(22),
31+
/// Literals section header is malformed.
32+
LITERALS_HEADER_WRONG(24),
33+
/// Dictionary content is corrupt.
34+
DICTIONARY_CORRUPTED(30),
35+
/// Frame was compressed with a different dictionary.
36+
DICTIONARY_WRONG(32),
37+
/// Dictionary could not be built.
38+
DICTIONARY_CREATION_FAILED(34),
39+
/// The given parameter is not supported.
40+
PARAMETER_UNSUPPORTED(40),
41+
/// The combination of parameters is not supported.
42+
PARAMETER_COMBINATION_UNSUPPORTED(41),
43+
/// A parameter value is outside its valid range.
44+
PARAMETER_OUT_OF_BOUND(42),
45+
/// Table log is too large.
46+
TABLE_LOG_TOO_LARGE(44),
47+
/// Maximum symbol value is too large.
48+
MAX_SYMBOL_VALUE_TOO_LARGE(46),
49+
/// Maximum symbol value is too small.
50+
MAX_SYMBOL_VALUE_TOO_SMALL(48),
51+
/// An uncompressed block could not be produced.
52+
CANNOT_PRODUCE_UNCOMPRESSED_BLOCK(49),
53+
/// A required stability condition was not respected.
54+
STABILITY_CONDITION_NOT_RESPECTED(50),
55+
/// Operation called at the wrong stage of the context lifecycle.
56+
STAGE_WRONG(60),
57+
/// Context was not initialised before use.
58+
INIT_MISSING(62),
59+
/// A memory allocation failed.
60+
MEMORY_ALLOCATION(64),
61+
/// The provided workspace is too small.
62+
WORKSPACE_TOO_SMALL(66),
63+
/// Destination buffer is too small to hold the result.
64+
DST_SIZE_TOO_SMALL(70),
65+
/// Declared source size does not match the data.
66+
SRC_SIZE_WRONG(72),
67+
/// Destination buffer pointer is NULL.
68+
DST_BUFFER_NULL(74),
69+
/// Streaming made no progress because the destination is full.
70+
NO_FORWARD_PROGRESS_DEST_FULL(80),
71+
/// Streaming made no progress because the input is empty.
72+
NO_FORWARD_PROGRESS_INPUT_EMPTY(82),
73+
/// Requested frame index is out of range (seekable format).
74+
FRAME_INDEX_TOO_LARGE(100),
75+
/// Seekable-format I/O error.
76+
SEEKABLE_IO(102),
77+
/// Destination buffer is otherwise invalid.
78+
DST_BUFFER_WRONG(104),
79+
/// Source buffer is otherwise invalid.
80+
SRC_BUFFER_WRONG(105),
81+
/// An external sequence producer failed.
82+
SEQUENCE_PRODUCER_FAILED(106),
83+
/// Externally supplied sequences are invalid.
84+
EXTERNAL_SEQUENCES_INVALID(107);
85+
86+
private final int value;
87+
88+
ZstdErrorCode(int value) {
89+
this.value = value;
90+
}
91+
92+
/// The native `ZSTD_ErrorCode` integer for this category.
93+
///
94+
/// @return the zstd error code value
95+
public int value() {
96+
return value;
97+
}
98+
99+
/// Maps a native `ZSTD_ErrorCode` integer to its category.
100+
///
101+
/// @param value the native error code
102+
/// @return the matching category, or [#UNKNOWN] if unrecognised
103+
static ZstdErrorCode of(int value) {
104+
for (ZstdErrorCode code : values()) {
105+
if (code.value == value) {
106+
return code;
107+
}
108+
}
109+
return UNKNOWN;
110+
}
111+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,23 @@ public final class ZstdException extends RuntimeException {
99

1010
private static final long serialVersionUID = 1L;
1111

12+
/// The zstd error category for this failure.
13+
private final ZstdErrorCode code;
14+
1215
ZstdException(String message) {
16+
this(message, ZstdErrorCode.UNKNOWN);
17+
}
18+
19+
ZstdException(String message, ZstdErrorCode code) {
1320
super(message);
21+
this.code = code;
22+
}
23+
24+
/// The category of this error, for programmatic branching.
25+
///
26+
/// @return the zstd error category, or [ZstdErrorCode#UNKNOWN] if the failure
27+
/// did not originate from a zstd error code
28+
public ZstdErrorCode code() {
29+
return code;
1430
}
1531
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.catchThrowableOfType;
10+
11+
class ZstdErrorTest {
12+
13+
private static final byte[] PAYLOAD = "typed error payload ".repeat(200).getBytes(StandardCharsets.UTF_8);
14+
15+
@Nested
16+
class Code {
17+
18+
@Test
19+
void reportsDstSizeTooSmall() {
20+
// Given a frame decompressed into too small a buffer
21+
byte[] frame = Zstd.compress(PAYLOAD);
22+
23+
// When it fails / Then the category is DST_SIZE_TOO_SMALL
24+
ZstdException ex = catchThrowableOfType(ZstdException.class, () -> Zstd.decompress(frame, 1));
25+
assertThat(ex.code()).isEqualTo(ZstdErrorCode.DST_SIZE_TOO_SMALL);
26+
}
27+
28+
@Test
29+
void reportsParameterOutOfBound() {
30+
try (ZstdCompressCtx ctx = new ZstdCompressCtx()) {
31+
ZstdException ex = catchThrowableOfType(ZstdException.class,
32+
() -> ctx.parameter(ZstdCompressParameter.WINDOW_LOG, 99));
33+
assertThat(ex.code()).isEqualTo(ZstdErrorCode.PARAMETER_OUT_OF_BOUND);
34+
}
35+
}
36+
37+
@Test
38+
void reportsANativeCategoryForGarbage() {
39+
// Given non-zstd input decompressed with a generous buffer
40+
byte[] garbage = "definitely not a zstd frame at all".getBytes(StandardCharsets.UTF_8);
41+
42+
// Then the error carries a real native category, not UNKNOWN
43+
ZstdException ex = catchThrowableOfType(ZstdException.class, () -> Zstd.decompress(garbage, 1024));
44+
assertThat(ex.code()).isNotIn(ZstdErrorCode.UNKNOWN, ZstdErrorCode.NO_ERROR);
45+
}
46+
}
47+
48+
@Nested
49+
class Mapping {
50+
51+
@Test
52+
void mapsKnownValues() {
53+
assertThat(ZstdErrorCode.of(22)).isEqualTo(ZstdErrorCode.CHECKSUM_WRONG);
54+
assertThat(ZstdErrorCode.of(70)).isEqualTo(ZstdErrorCode.DST_SIZE_TOO_SMALL);
55+
assertThat(ZstdErrorCode.CHECKSUM_WRONG.value()).isEqualTo(22);
56+
}
57+
58+
@Test
59+
void mapsUnknownValueToUnknown() {
60+
assertThat(ZstdErrorCode.of(999_999)).isEqualTo(ZstdErrorCode.UNKNOWN);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)