diff --git a/docs/how-to.md b/docs/how-to.md index ce61ea2..79e64f2 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -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 +# write the library into the matching native module's resources +./scripts/build-zstd.sh native//src/main/resources # classifier: osx-aarch64 | osx-x86_64 | linux-x86_64 | linux-aarch64 # | windows-x86_64 | windows-aarch64 + +./mvnw -pl native/ 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. diff --git a/zstd/src/main/java/io/github/dfa1/zstd/NativeLibrary.java b/zstd/src/main/java/io/github/dfa1/zstd/NativeLibrary.java index 1a735d2..e3f46d7 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/NativeLibrary.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/NativeLibrary.java @@ -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( @@ -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; @@ -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()); } diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java index 7f225c7..80d22f4 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java @@ -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 + "]"; + } } diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java index b5174a9..d3758be 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java @@ -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