Skip to content

Commit c4263b9

Browse files
Decompress: reuse the caller-supplied buffer before sizing from the frame
Decompress sized its destination from decompressSizeHint(src), which reads the frame's content-size field. Frames that do not carry that field — legacy zstd v0.5 frames, and streaming (unknown-pledged-size) frames — make the hint fall back to its upper bound (>= decompressSizeBufferLimit, i.e. 1 MB). When the caller passed a smaller-but-adequate buffer, Decompress discarded it and allocated that bound, so every such decode allocated at least 1 MB regardless of the real payload size. Try the caller-supplied buffer first via DecompressInto (which reports a too-small buffer without writing past it), and only fall back to the hint-sized allocation, then the stream API, when it does not fit. For frames that do advertise their size the behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent aad66fa commit c4263b9

2 files changed

Lines changed: 95 additions & 5 deletions

File tree

zstd.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,24 @@ func Decompress(dst, src []byte) ([]byte, error) {
136136
return []byte{}, ErrEmptySlice
137137
}
138138

139-
bound := decompressSizeHint(src)
140-
if cap(dst) >= bound {
141-
dst = dst[0:cap(dst)]
142-
} else {
143-
dst = make([]byte, bound)
139+
// If the caller supplied a buffer, try it as-is before sizing from the
140+
// frame header. Frames that do not carry their decompressed size (e.g.
141+
// legacy zstd v0.5 frames) make decompressSizeHint fall back to a large
142+
// upper bound, which would otherwise discard an adequate caller buffer and
143+
// allocate that bound instead. DecompressInto reports a too-small buffer
144+
// without writing past it, so this attempt is safe.
145+
if cap(dst) > 0 {
146+
written, err := DecompressInto(dst[:cap(dst)], src)
147+
if err == nil {
148+
return dst[:written], nil
149+
}
150+
if !IsDstSizeTooSmallError(err) {
151+
return nil, err
152+
}
144153
}
145154

155+
// No usable caller buffer; size from the frame hint.
156+
dst = make([]byte, decompressSizeHint(src))
146157
written, err := DecompressInto(dst, src)
147158
if err == nil {
148159
return dst[:written], nil

zstd_reuse_buffer_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package zstd
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
// unknownSizeFrame returns a zstd frame that does not advertise its decompressed
9+
// size in the header. The streaming writer produces such frames, as do legacy
10+
// zstd v0.5 frames; ZSTD_getFrameContentSize reports "unknown" for both, so
11+
// decompressSizeHint falls back to its (large) upper bound.
12+
func unknownSizeFrame(t *testing.T, payload []byte) []byte {
13+
t.Helper()
14+
var b bytes.Buffer
15+
w := NewWriter(&b)
16+
if _, err := w.Write(payload); err != nil {
17+
t.Fatalf("write: %v", err)
18+
}
19+
if err := w.Close(); err != nil {
20+
t.Fatalf("close: %v", err)
21+
}
22+
return b.Bytes()
23+
}
24+
25+
// TestDecompressReusesCallerBufferUnknownSize verifies that when a frame does
26+
// not advertise its content size, Decompress uses an adequately sized caller
27+
// buffer instead of discarding it and allocating decompressSizeBufferLimit.
28+
func TestDecompressReusesCallerBufferUnknownSize(t *testing.T) {
29+
payload := bytes.Repeat([]byte("datadog-"), 525) // 4200 bytes
30+
frame := unknownSizeFrame(t, payload)
31+
32+
// Sanity: the frame has unknown content size, so the hint is the upper
33+
// bound (>= decompressSizeBufferLimit). Without reusing the caller buffer,
34+
// Decompress would allocate that many bytes.
35+
if got := decompressSizeHint(frame); got < decompressSizeBufferLimit {
36+
t.Fatalf("expected unknown-size frame to hint the upper bound, got %d", got)
37+
}
38+
39+
buf := make([]byte, 8192) // adequate for the payload, far below the bound
40+
out, err := Decompress(buf, frame)
41+
if err != nil {
42+
t.Fatalf("Decompress: %v", err)
43+
}
44+
if !bytes.Equal(out, payload) {
45+
t.Fatalf("round-trip mismatch")
46+
}
47+
if cap(out) != cap(buf) {
48+
t.Fatalf("caller buffer (cap %d) should be reused, got cap %d", cap(buf), cap(out))
49+
}
50+
}
51+
52+
// TestDecompressUnknownSizeTooSmallBuffer verifies the fallback still works when
53+
// the caller buffer is too small for an unknown-size frame.
54+
func TestDecompressUnknownSizeTooSmallBuffer(t *testing.T) {
55+
payload := bytes.Repeat([]byte("datadog-"), 525)
56+
frame := unknownSizeFrame(t, payload)
57+
58+
out, err := Decompress(make([]byte, 8), frame) // too small; must fall back
59+
if err != nil {
60+
t.Fatalf("Decompress: %v", err)
61+
}
62+
if !bytes.Equal(out, payload) {
63+
t.Fatalf("round-trip mismatch on fallback")
64+
}
65+
}
66+
67+
// TestDecompressUnknownSizeNilBuffer verifies nil dst still decompresses.
68+
func TestDecompressUnknownSizeNilBuffer(t *testing.T) {
69+
payload := bytes.Repeat([]byte("datadog-"), 525)
70+
frame := unknownSizeFrame(t, payload)
71+
72+
out, err := Decompress(nil, frame)
73+
if err != nil {
74+
t.Fatalf("Decompress: %v", err)
75+
}
76+
if !bytes.Equal(out, payload) {
77+
t.Fatalf("round-trip mismatch on nil dst")
78+
}
79+
}

0 commit comments

Comments
 (0)