diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0f508..1c52e3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ git tags, which trigger publication to Maven Central. - `ZstdFrame.headerSize(byte[])` / `ZstdFrame.headerSize(MemorySegment)` — the size of a frame's header computed from just its leading bytes (as few as 5), without a full parse. Binds `ZSTD_frameHeaderSize`. +- `ZstdDictionary.compressDict(int)` / `compressDict()` / `decompressDict()` — + factories for digested dictionaries, e.g. `dict.compressDict(19)` instead of + `new ZstdCompressDict(dict, 19)`. They signal that the result is `AutoCloseable` + and are for sharing one digest across contexts via `refDictionary`; a single + context should prefer the context-owned `loadDictionary`. ### Changed - Every dictionary-id accessor now returns `ZstdDictionaryId` instead of `int`: diff --git a/docs/how-to.md b/docs/how-to.md index 245d129..d29414c 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -62,7 +62,7 @@ by reference — no per-call digesting, no copy. It pairs with `reset` for a pooled, recycled context: ```java -try (ZstdCompressDict cdict = new ZstdCompressDict(dict, 19)) { +try (ZstdCompressDict cdict = dict.compressDict(19)) { // one cctx per pooled worker, all sharing the one digested dictionary try (ZstdCompressCtx cctx = new ZstdCompressCtx()) { cctx.refDictionary(cdict); // borrowed; cdict must outlive cctx @@ -73,6 +73,13 @@ try (ZstdCompressDict cdict = new ZstdCompressDict(dict, 19)) { } ``` +`refDictionary` only borrows: the digested `cdict` is *not* tied to the context's +lifetime, so it must be closed separately (hence its own try-with-resources). That +is the price of sharing one digest across many contexts. If you have just **one** +context, don't build a `ZstdCompressDict` at all — `loadDictionary` above digests +into the context and frees it for you, and a stray, never-closed +`ZstdCompressDict` is a native-memory leak. + A loaded or referenced dictionary stays until replaced, cleared with `null`, or dropped by a parameter `reset`. `ZstdDecompressCtx` mirrors all of this. @@ -98,8 +105,8 @@ ZstdDictionary reloaded = ZstdDictionary.of(persisted); On a hot path, digest the dictionary once to skip per-call setup: ```java -try (ZstdCompressDict cdict = new ZstdCompressDict(dict, 19); - ZstdDecompressDict ddict = new ZstdDecompressDict(dict); +try (ZstdCompressDict cdict = dict.compressDict(19); + ZstdDecompressDict ddict = dict.decompressDict(); ZstdCompressCtx cctx = new ZstdCompressCtx(); ZstdDecompressCtx dctx = new ZstdDecompressCtx()) { byte[] packed = cctx.compress(record, cdict); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java index 5b20d7e..0622dda 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java @@ -297,6 +297,41 @@ public int size() { return bytes.length; } + /// Digests this dictionary once for compression at `level`, ready to share by + /// reference across contexts with [ZstdCompressCtx#refDictionary(ZstdCompressDict)]. + /// + /// The returned dictionary owns native memory — close it when done (it is + /// [AutoCloseable]). For a single context, prefer + /// [ZstdCompressCtx#loadDictionary(ZstdDictionary)], which the context digests, + /// owns, and frees for you. + /// + /// @param level the compression level to fix for the digested dictionary + /// @return a digested compression dictionary the caller must close + public ZstdCompressDict compressDict(int level) { + return new ZstdCompressDict(this, level); + } + + /// Digests this dictionary for compression at the library default level. + /// Otherwise identical to [#compressDict(int)]. + /// + /// @return a digested compression dictionary the caller must close + public ZstdCompressDict compressDict() { + return new ZstdCompressDict(this); + } + + /// Digests this dictionary once for decompression, ready to share by reference + /// across contexts with [ZstdDecompressCtx#refDictionary(ZstdDecompressDict)]. + /// + /// The returned dictionary owns native memory — close it when done (it is + /// [AutoCloseable]). For a single context, prefer + /// [ZstdDecompressCtx#loadDictionary(ZstdDictionary)], which the context owns + /// and frees for you. + /// + /// @return a digested decompression dictionary the caller must close + public ZstdDecompressDict decompressDict() { + return new ZstdDecompressDict(this); + } + /// Internal: direct view of the bytes for native calls. Not exposed. byte[] raw() { return bytes; diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java index 291d3a5..905054f 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java @@ -266,6 +266,58 @@ void interoperatesWithRawPath() { } } + @Nested + class Factories { + + @Test + void compressDictFixesTheRequestedLevel() { + // When a digested compress dictionary is built via the factory + try (ZstdCompressDict cdict = sut.compressDict(19)) { + // Then it carries that level and the dictionary's id + assertThat(cdict.level()).isEqualTo(19); + assertThat(cdict.id()).isEqualTo(sut.id()); + } + } + + @Test + void compressDictDefaultsToTheLibraryLevel() { + // When built without a level + try (ZstdCompressDict cdict = sut.compressDict()) { + // Then it uses the library default + assertThat(cdict.level()).isEqualTo(Zstd.defaultCompressionLevel()); + } + } + + @Test + void decompressDictReportsTheDictionaryId() { + // When a digested decompress dictionary is built via the factory + try (ZstdDecompressDict ddict = sut.decompressDict()) { + // Then it carries the dictionary's id + assertThat(ddict.id()).isEqualTo(sut.id()); + } + } + + @Test + void factoryDictionariesRoundTrip() { + // Given factory-built digested dictionaries + byte[] record = samples.get(123); + + byte[] restored; + try (ZstdCompressCtx cctx = new ZstdCompressCtx(); + ZstdDecompressCtx dctx = new ZstdDecompressCtx(); + ZstdCompressDict cdict = sut.compressDict(19); + ZstdDecompressDict ddict = sut.decompressDict()) { + + // When round-tripped through them + byte[] frame = cctx.compress(record, cdict); + restored = dctx.decompress(frame, record.length, ddict); + } + + // Then the record is recovered + assertThat(restored).isEqualTo(record); + } + } + @Nested class Serialisation {