Skip to content

Commit a5f4aa0

Browse files
dfa1claude
andauthored
fix: make ZstdSkippableContent defensively copy its bytes (#21)
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 <noreply@anthropic.com>
1 parent c9929b9 commit a5f4aa0

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

zstd/src/main/java/io/github/dfa1/zstd/ZstdSkippableContent.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@
99
/// @param magicVariant the variant 0..15 the frame was written with
1010
public record ZstdSkippableContent(byte[] content, int magicVariant) {
1111

12+
/// Defensively copies `content` so the record owns its bytes and cannot be
13+
/// mutated through the array the caller passed in.
14+
public ZstdSkippableContent {
15+
content = content.clone();
16+
}
17+
18+
/// The embedded content bytes, as a fresh copy so the record stays immutable.
19+
///
20+
/// @return a copy of the content bytes
21+
@Override
22+
public byte[] content() {
23+
return content.clone();
24+
}
25+
1226
/// Value equality over the payload and variant, comparing `content` by its
1327
/// bytes rather than by array identity (the record default).
1428
///

zstd/src/test/java/io/github/dfa1/zstd/ZstdFrameTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@ void standardFrameIsNotSkippable() {
142142
assertThat(ZstdFrame.isSkippableFrame(Zstd.compress(PAYLOAD))).isFalse();
143143
}
144144

145+
@Test
146+
void defensivelyCopiesContentInAndOut() {
147+
// Given a backing array wrapped in a skippable-content value
148+
byte[] backing = "metadata".getBytes(StandardCharsets.UTF_8);
149+
ZstdSkippableContent content = new ZstdSkippableContent(backing, 2);
150+
151+
// When the source array and a value returned by the accessor are mutated
152+
backing[0] = 'X';
153+
content.content()[1] = 'X';
154+
155+
// Then the record's own bytes are untouched
156+
assertThat(content.content()).isEqualTo("metadata".getBytes(StandardCharsets.UTF_8));
157+
}
158+
145159
@Test
146160
void contentHasValueEqualityOverTheBytesNotArrayIdentity() {
147161
// Given two separately built payloads with the same bytes and variant, and one differing

0 commit comments

Comments
 (0)