Skip to content

Commit ea1e0ce

Browse files
dfa1claude
andcommitted
refactor: ZstdByteSize for ZstdFrameHeader's bounded size fields
Continues the value-object stabilization into ZstdFrameHeader: - blockSizeMax(): int-masked long -> ZstdByteSize (masked, always non-negative) - headerSize(): int -> ZstdByteSize (now matches ZstdFrame.headerSize()) frameContentSize() and windowSize() deliberately stay raw long: a hostile header can make either read back as an unsigned value at or above 2^63 (zstd copies the content-size field verbatim, and for a single-segment frame sets windowSize equal to it), which wraps negative in a signed long. Wrapping those in a non-negative ZstdByteSize would turn ZstdFrame.header() parsing into a thrown exception on hostile input — a regression the sign-bit test catches. The typed contentSize() remains the safe view of the content size. Also resolves the headerSize() asymmetry from the previous PR: ZstdFrame.headerSize() and ZstdFrameHeader.headerSize() now both return ZstdByteSize and compare directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fb1d9f4 commit ea1e0ce

5 files changed

Lines changed: 27 additions & 13 deletions

File tree

.github/smoke/src/test/java/SmokeTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ void frameIntrospection() {
184184
check(header.contentSize().isPresent() && header.contentSize().get().value() == original.length,
185185
"header(byte[]).contentSize() mismatch");
186186
check(header.frameType() == ZstdFrameType.STANDARD, "header(byte[]).frameType() expected STANDARD");
187-
check(header.headerSize() == headerSize, "header(byte[]).headerSize() disagreed with headerSize(byte[])");
187+
check(header.headerSize().value() == headerSize, "header(byte[]).headerSize() disagreed with headerSize(byte[])");
188188
check(!header.hasChecksum(), "header(byte[]).hasChecksum() expected false (checksum not enabled)");
189189
}
190190

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ git tags, which trigger publication to Maven Central.
4949
`String`, and `Zstd.versionNumber()` is removed. Use
5050
`Zstd.version().toString()` for the `x.y.z` string and
5151
`Zstd.version().number()` for the packed number.
52+
- **Breaking:** `ZstdFrameHeader.blockSizeMax()` and `headerSize()` now return
53+
`ZstdByteSize` (were `long`/`int`); call `.value()` for the raw number.
54+
`frameContentSize()` and `windowSize()` stay raw `long` — a hostile header can
55+
make either read back as an unrepresentable value at or above `2^63`, so
56+
wrapping them in a non-negative `ZstdByteSize` would turn a parse into a thrown
57+
exception; use the typed `contentSize()` for the content size.
5258

5359
## [0.11] - 2026-07-24
5460

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,13 +259,13 @@ private static ZstdFrameHeader header(MemorySegment data, long size) {
259259
throw new ZstdException("incomplete frame header: need " + remaining + " more bytes");
260260
}
261261
return new ZstdFrameHeader(
262-
zfh.get(JAVA_LONG, 0), // frameContentSize
263-
zfh.get(JAVA_LONG, 8), // windowSize
264-
zfh.get(JAVA_INT, 16) & 0xFFFFFFFFL, // blockSizeMax
265-
ZstdFrameType.of(zfh.get(JAVA_INT, 20)), // frameType
266-
zfh.get(JAVA_INT, 24), // headerSize
262+
zfh.get(JAVA_LONG, 0), // frameContentSize
263+
zfh.get(JAVA_LONG, 8), // windowSize
264+
new ZstdByteSize(zfh.get(JAVA_INT, 16) & 0xFFFFFFFFL), // blockSizeMax
265+
ZstdFrameType.of(zfh.get(JAVA_INT, 20)), // frameType
266+
new ZstdByteSize(zfh.get(JAVA_INT, 24)), // headerSize
267267
ZstdDictionaryId.of(zfh.get(JAVA_INT, 28)), // dictID
268-
zfh.get(JAVA_INT, 32) != 0); // checksumFlag
268+
zfh.get(JAVA_INT, 32) != 0); // checksumFlag
269269
}
270270
}
271271

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44

55
/// Parsed contents of a zstd frame header, from `ZSTD_getFrameHeader`.
66
///
7-
/// @param frameContentSize the stored decompressed size, or a sentinel if unknown;
8-
/// prefer [#contentSize()]
7+
/// `frameContentSize` and `windowSize` stay raw `long`s rather than
8+
/// [ZstdByteSize]: both can read back as an unrepresentable value on a hostile
9+
/// header (zstd copies the 8-byte content-size field verbatim, and for a
10+
/// single-segment frame sets `windowSize` equal to it), so an unsigned value at
11+
/// or above `2^63` wraps negative in a signed `long`. Wrapping either in a
12+
/// non-negative [ZstdByteSize] here would turn a parse into a thrown exception —
13+
/// use the typed [#contentSize()] for the safe view of the content size.
14+
///
15+
/// @param frameContentSize the raw stored decompressed size, or a sentinel if
16+
/// unknown — prefer the typed [#contentSize()]
917
/// @param windowSize the back-reference window size needed to decode the frame
1018
/// @param blockSizeMax the maximum block size used in the frame
1119
/// @param frameType whether this is a standard or skippable frame
@@ -16,9 +24,9 @@
1624
public record ZstdFrameHeader(
1725
long frameContentSize,
1826
long windowSize,
19-
long blockSizeMax,
27+
ZstdByteSize blockSizeMax,
2028
ZstdFrameType frameType,
21-
int headerSize,
29+
ZstdByteSize headerSize,
2230
ZstdDictionaryId dictId,
2331
boolean hasChecksum) {
2432

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ void matchesTheParsedHeaderSize() {
172172
byte[] frame = Zstd.compress(PAYLOAD);
173173

174174
// Then the light probe agrees with the fully parsed header
175-
assertThat(ZstdFrame.headerSize(frame)).isEqualTo(new ZstdByteSize(ZstdFrame.header(frame).headerSize()));
175+
assertThat(ZstdFrame.headerSize(frame)).isEqualTo(ZstdFrame.header(frame).headerSize());
176176
}
177177

178178
@Test
@@ -297,7 +297,7 @@ void reportsASaneBlockSizeMax() {
297297

298298
// Then blockSizeMax is the masked 32-bit field — a real block size, never
299299
// the all-ones value a sign-extension or wrong mask would produce
300-
assertThat(header.blockSizeMax()).isPositive().isLessThanOrEqualTo(128 * 1024L);
300+
assertThat(header.blockSizeMax().value()).isPositive().isLessThanOrEqualTo(128 * 1024L);
301301
}
302302

303303
@Test

0 commit comments

Comments
 (0)