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
20 changes: 11 additions & 9 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,19 @@ this pays off, see the [explanation](zero-copy.md).

## Run against a self-built libzstd

To use a `libzstd` you built yourself instead of the bundled one, point the
loader at it:
The loader only ever loads the library bundled in the platform native jar on the
classpath — there is no path override. Loading a caller-supplied native library
would be arbitrary native code execution in the JVM, so to use a `libzstd` you
built yourself, build it *into* that resource and rebuild the jar:

```bash
java -Dzstd.lib.path=/path/to/libzstd.dylib --enable-native-access=ALL-UNNAMED ...
```

Build any of the six targets from any host:

```bash
./scripts/build-zstd.sh <output-resources-dir> <classifier>
# write the library into the matching native module's resources
./scripts/build-zstd.sh native/<classifier>/src/main/resources <classifier>
# classifier: osx-aarch64 | osx-x86_64 | linux-x86_64 | linux-aarch64
# | windows-x86_64 | windows-aarch64

./mvnw -pl native/<classifier> install # repackage the native jar
```

The bundled `.dylib/.so/.dll` are git-ignored and regenerated from the submodule,
so this just overwrites the artifact the loader already trusts.
31 changes: 18 additions & 13 deletions zstd/src/main/java/io/github/dfa1/zstd/NativeLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
/// Infrastructure — loads the bundled `libzstd` shared library and binds
/// native symbols to {@link MethodHandle}s via the Foreign Function & Memory API.
///
/// The library is resolved from the platform-specific native JAR on the
/// The library is resolved only from the platform-specific native JAR on the
/// classpath, extracted to a temp file, and loaded once at class-init time.
/// Override with `-Dzstd.lib.path=/path/to/libzstd.so`.
///
/// There is deliberately no path override (no `-Dzstd.lib.path`): loading a
/// caller-supplied native library is arbitrary native code execution in the JVM
/// process, so the loader trusts only the signed artifact on the classpath. To
/// run a self-built `libzstd`, package it into the native resource jar — see
/// `docs/how-to.md`.
@SuppressWarnings("restricted") // libraryLookup / downcallHandle are restricted FFM methods
final class NativeLibrary {

private static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup LIB = SymbolLookup.libraryLookup(resolveLibPath(), Arena.ofAuto());
private static final SymbolLookup LIB = SymbolLookup.libraryLookup(extractBundledLib(), Arena.ofAuto());

static MethodHandle lookup(String name, FunctionDescriptor fd) {
return LINKER.downcallHandle(
Expand All @@ -30,12 +35,7 @@ static MethodHandle lookup(String name, FunctionDescriptor fd) {
fd);
}

private static String resolveLibPath() {
String explicit = System.getProperty("zstd.lib.path");
if (explicit != null) {
return explicit;
}

private static String extractBundledLib() {
String classifier = classifier();
String ext = libExtension(classifier);
String resource = "/native/" + classifier + "/libzstd." + ext;
Expand All @@ -44,10 +44,15 @@ private static String resolveLibPath() {
if (in == null) {
throw new UnsatisfiedLinkError("No bundled zstd library found for platform " + classifier);
}
Path tmp = Files.createTempFile("libzstd-", "." + ext);
tmp.toFile().deleteOnExit();
Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING);
return tmp.toString();
// Extract into a private, owner-only temp directory rather than a file
// loose in the shared temp root: createTempDirectory is 0700 on POSIX, so
// no other local user can swap the library between extraction and dlopen.
Path dir = Files.createTempDirectory("zstd-");
Path lib = dir.resolve("libzstd." + ext);
Files.copy(in, lib, StandardCopyOption.REPLACE_EXISTING);
lib.toFile().deleteOnExit();
dir.toFile().deleteOnExit();
return lib.toString();
} catch (IOException e) {
throw new UnsatisfiedLinkError("Failed to extract bundled zstd: " + e.getMessage());
}
Expand Down
32 changes: 32 additions & 0 deletions zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
package io.github.dfa1.zstd;

import java.util.Arrays;

/// The decoded payload of a skippable frame, returned by
/// [ZstdFrame#readSkippableFrame(byte[])].
///
/// @param content the user bytes carried by the skippable frame
/// @param magicVariant the variant 0..15 the frame was written with
public record ZstdSkippableContent(byte[] content, int magicVariant) {

/// Value equality over the payload and variant, comparing `content` by its
/// bytes rather than by array identity (the record default).
///
/// @param o the object to compare with
/// @return `true` if `o` is a [ZstdSkippableContent] with equal content bytes and variant
@Override
public boolean equals(Object o) {
return o instanceof ZstdSkippableContent other
&& magicVariant == other.magicVariant
&& Arrays.equals(content, other.content);
}

/// Hash code consistent with [#equals(Object)], derived from the content bytes
/// and the variant.
///
/// @return the content-based hash code
@Override
public int hashCode() {
return 31 * Arrays.hashCode(content) + magicVariant;
}

/// Short description carrying the payload length and variant rather than the
/// array's identity hash.
///
/// @return a string with the content length and magic variant
@Override
public String toString() {
return "ZstdSkippableContent[content=" + content.length + " bytes, magicVariant=" + magicVariant + "]";
}
}
18 changes: 18 additions & 0 deletions zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ void roundTripsContentAndMagicVariant() {
void standardFrameIsNotSkippable() {
assertThat(ZstdFrame.isSkippableFrame(Zstd.compress(PAYLOAD))).isFalse();
}

@Test
void contentHasValueEqualityOverTheBytesNotArrayIdentity() {
// Given two separately built payloads with the same bytes and variant, and one differing
ZstdSkippableContent a = new ZstdSkippableContent("meta".getBytes(StandardCharsets.UTF_8), 3);
ZstdSkippableContent b = new ZstdSkippableContent("meta".getBytes(StandardCharsets.UTF_8), 3);
ZstdSkippableContent differentVariant = new ZstdSkippableContent("meta".getBytes(StandardCharsets.UTF_8), 4);

// When compared by value and rendered as text
boolean sameBytesEqual = a.equals(b);
boolean differentVariantEqual = a.equals(differentVariant);

// Then equality and hashCode follow the content bytes, and toString omits the identity hash
assertThat(sameBytesEqual).isTrue();
assertThat(differentVariantEqual).isFalse();
assertThat(a).hasSameHashCodeAs(b);
assertThat(a).hasToString("ZstdSkippableContent[content=4 bytes, magicVariant=3]");
}
}

@Nested
Expand Down
Loading