From 73e7041b3135a042ecf4833929a208fc5b78103d Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Fri, 26 Jun 2026 22:29:08 +0200 Subject: [PATCH] fix: make ZstdSkippableContent defensively copy its bytes The record stored the caller's array and handed it back from content() uncopied, so a "value" object could be mutated through the input array or the accessor result. Clone in the compact constructor and in the content() accessor, matching ZstdDictionary's copy-in/copy-out. Add a test that mutating the source array and the accessor result leaves the record untouched. Co-Authored-By: Claude Opus 4.8 --- .../io/github/dfa1/zstd/ZstdSkippableContent.java | 14 ++++++++++++++ .../java/io/github/dfa1/zstd/ZstdFrameTest.java | 14 ++++++++++++++ 2 files changed, 28 insertions(+) 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 9b892a6..83a3a11 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java @@ -9,6 +9,20 @@ /// @param magicVariant the variant 0..15 the frame was written with public record ZstdSkippableContent(byte[] content, int magicVariant) { + /// Defensively copies `content` so the record owns its bytes and cannot be + /// mutated through the array the caller passed in. + public ZstdSkippableContent { + content = content.clone(); + } + + /// The embedded content bytes, as a fresh copy so the record stays immutable. + /// + /// @return a copy of the content bytes + @Override + public byte[] content() { + return content.clone(); + } + /// Value equality over the payload and variant, comparing `content` by its /// bytes rather than by array identity (the record default). /// 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 0add02f..1aee6da 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java @@ -142,6 +142,20 @@ void standardFrameIsNotSkippable() { assertThat(ZstdFrame.isSkippableFrame(Zstd.compress(PAYLOAD))).isFalse(); } + @Test + void defensivelyCopiesContentInAndOut() { + // Given a backing array wrapped in a skippable-content value + byte[] backing = "metadata".getBytes(StandardCharsets.UTF_8); + ZstdSkippableContent content = new ZstdSkippableContent(backing, 2); + + // When the source array and a value returned by the accessor are mutated + backing[0] = 'X'; + content.content()[1] = 'X'; + + // Then the record's own bytes are untouched + assertThat(content.content()).isEqualTo("metadata".getBytes(StandardCharsets.UTF_8)); + } + @Test void contentHasValueEqualityOverTheBytesNotArrayIdentity() { // Given two separately built payloads with the same bytes and variant, and one differing