Add WithLimit methods for uncompression (#2147)

* Add WithLimit methods for uncompression

The current uncompress methods don't enforce a memory limit and are
susceptible to things like zip bombs. This pull introduces new methods
so retain backwards compatibility. The old methods might be deprecated
in the future.

* Fix suggestion
This commit is contained in:
Erik Dubbelboer
2026-02-22 18:13:40 +01:00
committed by GitHub
parent c2b317d47d
commit f0d5d9a5cb
7 changed files with 338 additions and 36 deletions
+1
View File
@@ -379,6 +379,7 @@ Important points:
- [r.FormValue()](https://pkg.go.dev/net/http#Request.FormValue) **➜** [ctx.FormValue()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.FormValue)
- [r.FormFile()](https://pkg.go.dev/net/http#Request.FormFile) **➜** [ctx.FormFile()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.FormFile)
- [r.MultipartForm](https://pkg.go.dev/net/http#Request) **➜** [ctx.MultipartForm()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.MultipartForm)
For untrusted multipart input, use [ctx.MultipartFormWithLimit()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.MultipartFormWithLimit) (or a custom [Server.FormValueFunc](https://pkg.go.dev/github.com/valyala/fasthttp#Server)) to enforce a parsing size limit.
- [r.RemoteAddr](https://pkg.go.dev/net/http#Request) **➜** [ctx.RemoteAddr()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.RemoteAddr)
- [r.RequestURI](https://pkg.go.dev/net/http#Request) **➜** [ctx.RequestURI()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.RequestURI)
- [r.TLS](https://pkg.go.dev/net/http#Request) **➜** [ctx.IsTLS()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.IsTLS)
+5 -1
View File
@@ -167,12 +167,16 @@ func AppendBrotliBytes(dst, src []byte) []byte {
// WriteUnbrotli writes unbrotlied p to w and returns the number of uncompressed
// bytes written to w.
func WriteUnbrotli(w io.Writer, p []byte) (int, error) {
return writeUnbrotli(w, p, 0)
}
func writeUnbrotli(w io.Writer, p []byte, maxBodySize int) (int, error) {
r := &byteSliceReader{b: p}
zr, err := acquireBrotliReader(r)
if err != nil {
return 0, err
}
n, err := copyZeroAlloc(w, zr)
n, err := copyZeroAllocWithLimit(w, zr, maxBodySize)
releaseBrotliReader(zr)
nn := int(n)
if int64(nn) != n {
+10 -2
View File
@@ -212,12 +212,16 @@ func AppendGzipBytes(dst, src []byte) []byte {
// WriteGunzip writes ungzipped p to w and returns the number of uncompressed
// bytes written to w.
func WriteGunzip(w io.Writer, p []byte) (int, error) {
return writeGunzip(w, p, 0)
}
func writeGunzip(w io.Writer, p []byte, maxBodySize int) (int, error) {
r := &byteSliceReader{b: p}
zr, err := acquireGzipReader(r)
if err != nil {
return 0, err
}
n, err := copyZeroAlloc(w, zr)
n, err := copyZeroAllocWithLimit(w, zr, maxBodySize)
releaseGzipReader(zr)
nn := int(n)
if int64(nn) != n {
@@ -321,12 +325,16 @@ func AppendDeflateBytes(dst, src []byte) []byte {
// WriteInflate writes inflated p to w and returns the number of uncompressed
// bytes written to w.
func WriteInflate(w io.Writer, p []byte) (int, error) {
return writeInflate(w, p, 0)
}
func writeInflate(w io.Writer, p []byte, maxBodySize int) (int, error) {
r := &byteSliceReader{b: p}
zr, err := acquireFlateReader(r)
if err != nil {
return 0, err
}
n, err := copyZeroAlloc(w, zr)
n, err := copyZeroAllocWithLimit(w, zr, maxBodySize)
releaseFlateReader(zr)
nn := int(n)
if int64(nn) != n {
+163 -29
View File
@@ -492,7 +492,15 @@ var (
// 'Content-Encoding: gzip' for reading un-gzipped body.
// Use Body for reading gzipped request body.
func (req *Request) BodyGunzip() ([]byte, error) {
return gunzipData(req.Body())
return req.BodyGunzipWithLimit(0)
}
// BodyGunzipWithLimit returns un-gzipped body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (req *Request) BodyGunzipWithLimit(maxBodySize int) ([]byte, error) {
return gunzipData(req.Body(), maxBodySize)
}
// BodyGunzip returns un-gzipped body data.
@@ -501,12 +509,20 @@ func (req *Request) BodyGunzip() ([]byte, error) {
// 'Content-Encoding: gzip' for reading un-gzipped body.
// Use Body for reading gzipped response body.
func (resp *Response) BodyGunzip() ([]byte, error) {
return gunzipData(resp.Body())
return resp.BodyGunzipWithLimit(0)
}
func gunzipData(p []byte) ([]byte, error) {
// BodyGunzipWithLimit returns un-gzipped body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (resp *Response) BodyGunzipWithLimit(maxBodySize int) ([]byte, error) {
return gunzipData(resp.Body(), maxBodySize)
}
func gunzipData(p []byte, maxBodySize int) ([]byte, error) {
var bb bytebufferpool.ByteBuffer
_, err := WriteGunzip(&bb, p)
_, err := writeGunzip(&bb, p, maxBodySize)
if err != nil {
return nil, err
}
@@ -519,7 +535,15 @@ func gunzipData(p []byte) ([]byte, error) {
// 'Content-Encoding: br' for reading un-brotlied body.
// Use Body for reading brotlied request body.
func (req *Request) BodyUnbrotli() ([]byte, error) {
return unBrotliData(req.Body())
return req.BodyUnbrotliWithLimit(0)
}
// BodyUnbrotliWithLimit returns un-brotlied body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (req *Request) BodyUnbrotliWithLimit(maxBodySize int) ([]byte, error) {
return unBrotliData(req.Body(), maxBodySize)
}
// BodyUnbrotli returns un-brotlied body data.
@@ -528,12 +552,20 @@ func (req *Request) BodyUnbrotli() ([]byte, error) {
// 'Content-Encoding: br' for reading un-brotlied body.
// Use Body for reading brotlied response body.
func (resp *Response) BodyUnbrotli() ([]byte, error) {
return unBrotliData(resp.Body())
return resp.BodyUnbrotliWithLimit(0)
}
func unBrotliData(p []byte) ([]byte, error) {
// BodyUnbrotliWithLimit returns un-brotlied body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (resp *Response) BodyUnbrotliWithLimit(maxBodySize int) ([]byte, error) {
return unBrotliData(resp.Body(), maxBodySize)
}
func unBrotliData(p []byte, maxBodySize int) ([]byte, error) {
var bb bytebufferpool.ByteBuffer
_, err := WriteUnbrotli(&bb, p)
_, err := writeUnbrotli(&bb, p, maxBodySize)
if err != nil {
return nil, err
}
@@ -546,7 +578,15 @@ func unBrotliData(p []byte) ([]byte, error) {
// 'Content-Encoding: deflate' for reading inflated request body.
// Use Body for reading deflated request body.
func (req *Request) BodyInflate() ([]byte, error) {
return inflateData(req.Body())
return req.BodyInflateWithLimit(0)
}
// BodyInflateWithLimit returns inflated body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (req *Request) BodyInflateWithLimit(maxBodySize int) ([]byte, error) {
return inflateData(req.Body(), maxBodySize)
}
// BodyInflate returns inflated body data.
@@ -555,7 +595,15 @@ func (req *Request) BodyInflate() ([]byte, error) {
// 'Content-Encoding: deflate' for reading inflated response body.
// Use Body for reading deflated response body.
func (resp *Response) BodyInflate() ([]byte, error) {
return inflateData(resp.Body())
return resp.BodyInflateWithLimit(0)
}
// BodyInflateWithLimit returns inflated body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (resp *Response) BodyInflateWithLimit(maxBodySize int) ([]byte, error) {
return inflateData(resp.Body(), maxBodySize)
}
func (ctx *RequestCtx) RequestBodyStream() io.Reader {
@@ -563,25 +611,41 @@ func (ctx *RequestCtx) RequestBodyStream() io.Reader {
}
func (req *Request) BodyUnzstd() ([]byte, error) {
return unzstdData(req.Body())
return req.BodyUnzstdWithLimit(0)
}
// BodyUnzstdWithLimit returns un-zstd body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (req *Request) BodyUnzstdWithLimit(maxBodySize int) ([]byte, error) {
return unzstdData(req.Body(), maxBodySize)
}
func (resp *Response) BodyUnzstd() ([]byte, error) {
return unzstdData(resp.Body())
return resp.BodyUnzstdWithLimit(0)
}
func unzstdData(p []byte) ([]byte, error) {
// BodyUnzstdWithLimit returns un-zstd body data and limits the size
// of uncompressed body data to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (resp *Response) BodyUnzstdWithLimit(maxBodySize int) ([]byte, error) {
return unzstdData(resp.Body(), maxBodySize)
}
func unzstdData(p []byte, maxBodySize int) ([]byte, error) {
var bb bytebufferpool.ByteBuffer
_, err := WriteUnzstd(&bb, p)
_, err := writeUnzstd(&bb, p, maxBodySize)
if err != nil {
return nil, err
}
return bb.B, nil
}
func inflateData(p []byte) ([]byte, error) {
func inflateData(p []byte, maxBodySize int) ([]byte, error) {
var bb bytebufferpool.ByteBuffer
_, err := WriteInflate(&bb, p)
_, err := writeInflate(&bb, p, maxBodySize)
if err != nil {
return nil, err
}
@@ -590,45 +654,63 @@ func inflateData(p []byte) ([]byte, error) {
var ErrContentEncodingUnsupported = errors.New("unsupported Content-Encoding")
// BodyUncompressed returns body data and if needed decompress it from gzip, deflate or Brotli.
// BodyUncompressed returns body data and if needed decompresses it from gzip,
// deflate, brotli or zstd.
//
// This method may be used if the response header contains
// 'Content-Encoding' for reading uncompressed request body.
// Use Body for reading the raw request body.
func (req *Request) BodyUncompressed() ([]byte, error) {
return req.BodyUncompressedWithLimit(0)
}
// BodyUncompressedWithLimit returns body data and if needed decompresses it from gzip,
// deflate, brotli or zstd. The size of uncompressed data is limited to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (req *Request) BodyUncompressedWithLimit(maxBodySize int) ([]byte, error) {
switch string(req.Header.ContentEncoding()) {
case "":
return req.Body(), nil
case "deflate":
return req.BodyInflate()
return req.BodyInflateWithLimit(maxBodySize)
case "gzip":
return req.BodyGunzip()
return req.BodyGunzipWithLimit(maxBodySize)
case "br":
return req.BodyUnbrotli()
return req.BodyUnbrotliWithLimit(maxBodySize)
case "zstd":
return req.BodyUnzstd()
return req.BodyUnzstdWithLimit(maxBodySize)
default:
return nil, ErrContentEncodingUnsupported
}
}
// BodyUncompressed returns body data and if needed decompress it from gzip, deflate or Brotli.
// BodyUncompressed returns body data and if needed decompresses it from gzip,
// deflate, brotli or zstd.
//
// This method may be used if the response header contains
// 'Content-Encoding' for reading uncompressed response body.
// Use Body for reading the raw response body.
func (resp *Response) BodyUncompressed() ([]byte, error) {
return resp.BodyUncompressedWithLimit(0)
}
// BodyUncompressedWithLimit returns body data and if needed decompresses it from gzip,
// deflate, brotli or zstd. The size of uncompressed data is limited to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
func (resp *Response) BodyUncompressedWithLimit(maxBodySize int) ([]byte, error) {
switch string(resp.Header.ContentEncoding()) {
case "":
return resp.Body(), nil
case "deflate":
return resp.BodyInflate()
return resp.BodyInflateWithLimit(maxBodySize)
case "gzip":
return resp.BodyGunzip()
return resp.BodyGunzipWithLimit(maxBodySize)
case "br":
return resp.BodyUnbrotli()
return resp.BodyUnbrotliWithLimit(maxBodySize)
case "zstd":
return resp.BodyUnzstd()
return resp.BodyUnzstdWithLimit(maxBodySize)
default:
return nil, ErrContentEncodingUnsupported
}
@@ -1009,9 +1091,26 @@ var ErrNoMultipartForm = errors.New("request Content-Type has bad boundary or is
// Returns ErrNoMultipartForm if request's Content-Type
// isn't 'multipart/form-data'.
//
// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size
// limit is applied during multipart parsing.
//
// RemoveMultipartFormFiles must be called after returned multipart form
// is processed.
func (req *Request) MultipartForm() (*multipart.Form, error) {
return req.MultipartFormWithLimit(0)
}
// MultipartFormWithLimit returns request's multipart form and limits the
// read multipart body size to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
//
// Returns ErrNoMultipartForm if request's Content-Type
// isn't 'multipart/form-data'.
//
// RemoveMultipartFormFiles must be called after returned multipart form
// is processed.
func (req *Request) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) {
if req.multipartForm != nil {
return req.multipartForm, nil
}
@@ -1026,30 +1125,46 @@ func (req *Request) MultipartForm() (*multipart.Form, error) {
if req.bodyStream != nil {
bodyStream := req.bodyStream
var lr *io.LimitedReader
if bytes.Equal(ce, strGzip) {
// Do not care about memory usage here.
if bodyStream, err = gzip.NewReader(bodyStream); err != nil {
return nil, fmt.Errorf("cannot gunzip request body: %w", err)
}
} else if len(ce) > 0 {
return nil, fmt.Errorf("unsupported Content-Encoding: %q", ce)
}
if maxBodySize > 0 {
lr = &io.LimitedReader{
R: bodyStream,
N: int64(maxBodySize) + 1,
}
bodyStream = lr
}
mr := multipart.NewReader(bodyStream, req.multipartFormBoundary)
req.multipartForm, err = mr.ReadForm(8 * 1024)
if err != nil {
if lr != nil && lr.N <= 0 {
return nil, fmt.Errorf("cannot read multipart/form-data body: %w", ErrBodyTooLarge)
}
return nil, fmt.Errorf("cannot read multipart/form-data body: %w", err)
}
if lr != nil && lr.N <= 0 {
req.RemoveMultipartFormFiles()
return nil, fmt.Errorf("cannot read multipart/form-data body: %w", ErrBodyTooLarge)
}
} else {
body := req.bodyBytes()
if bytes.Equal(ce, strGzip) {
// Do not care about memory usage here.
if body, err = AppendGunzipBytes(nil, body); err != nil {
if body, err = gunzipData(body, maxBodySize); err != nil {
return nil, fmt.Errorf("cannot gunzip request body: %w", err)
}
} else if len(ce) > 0 {
return nil, fmt.Errorf("unsupported Content-Encoding: %q", ce)
}
if maxBodySize > 0 && len(body) > maxBodySize {
return nil, fmt.Errorf("cannot read multipart/form-data body: %w", ErrBodyTooLarge)
}
req.multipartForm, err = readMultipartForm(bytes.NewReader(body), req.multipartFormBoundary, len(body), len(body))
if err != nil {
@@ -2464,6 +2579,25 @@ func writeChunk(w *bufio.Writer, b []byte) error {
// the given limit.
var ErrBodyTooLarge = errors.New("body size exceeds the given limit")
func copyZeroAllocWithLimit(w io.Writer, r io.Reader, maxBodySize int) (int64, error) {
if maxBodySize <= 0 {
return copyZeroAlloc(w, r)
}
lr := &io.LimitedReader{
R: r,
N: int64(maxBodySize) + 1,
}
n, err := copyZeroAlloc(w, lr)
if err != nil {
return n, err
}
if lr.N <= 0 {
return n, ErrBodyTooLarge
}
return n, nil
}
func readBody(r *bufio.Reader, contentLength, maxBodySize int, dst []byte) ([]byte, error) {
if maxBodySize > 0 && contentLength > maxBodySize {
return dst, ErrBodyTooLarge
+125
View File
@@ -448,6 +448,131 @@ func TestResponseBodyUncompressed(t *testing.T) {
}
}
func TestBodyDecodeWithLimitTooLarge(t *testing.T) {
t.Parallel()
body := bytes.Repeat([]byte("a"), 2*1024)
maxBodySize := 1024
testCases := []struct {
name string
encoding string
encode func([]byte) []byte
}{
{
name: "gzip",
encoding: "gzip",
encode: func(src []byte) []byte {
return AppendGzipBytes(nil, src)
},
},
{
name: "deflate",
encoding: "deflate",
encode: func(src []byte) []byte {
return AppendDeflateBytes(nil, src)
},
},
{
name: "brotli",
encoding: "br",
encode: func(src []byte) []byte {
return AppendBrotliBytes(nil, src)
},
},
{
name: "zstd",
encoding: "zstd",
encode: func(src []byte) []byte {
return AppendZstdBytes(nil, src)
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name+"_request_uncompressed", func(t *testing.T) {
var req Request
req.Header.SetContentEncoding(testCase.encoding)
req.SetBodyRaw(testCase.encode(body))
_, err := req.BodyUncompressedWithLimit(maxBodySize)
if !errors.Is(err, ErrBodyTooLarge) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run(testCase.name+"_response_uncompressed", func(t *testing.T) {
var resp Response
resp.Header.SetContentEncoding(testCase.encoding)
resp.SetBodyRaw(testCase.encode(body))
_, err := resp.BodyUncompressedWithLimit(maxBodySize)
if !errors.Is(err, ErrBodyTooLarge) {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestRequestMultipartFormWithLimitGzip(t *testing.T) {
t.Parallel()
var formBodyBuffer bytes.Buffer
mw := multipart.NewWriter(&formBodyBuffer)
if err := mw.WriteField("foo", strings.Repeat("a", 8*1024)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
boundary := mw.Boundary()
if err := mw.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
formBody := formBodyBuffer.Bytes()
gzippedBody := AppendGzipBytes(nil, formBody)
t.Run("buffered_too_large", func(t *testing.T) {
var req Request
req.Header.SetMultipartFormBoundary(boundary)
req.Header.SetContentEncoding("gzip")
req.SetBodyRaw(gzippedBody)
_, err := req.MultipartFormWithLimit(len(formBody) - 1)
if !errors.Is(err, ErrBodyTooLarge) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("streamed_too_large", func(t *testing.T) {
var req Request
req.Header.SetMultipartFormBoundary(boundary)
req.Header.SetContentEncoding("gzip")
req.SetBodyStream(bytes.NewReader(gzippedBody), len(gzippedBody))
_, err := req.MultipartFormWithLimit(len(formBody) - 1)
if !errors.Is(err, ErrBodyTooLarge) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("buffered_success", func(t *testing.T) {
var req Request
req.Header.SetMultipartFormBoundary(boundary)
req.Header.SetContentEncoding("gzip")
req.SetBodyRaw(gzippedBody)
f, err := req.MultipartFormWithLimit(len(formBody))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer req.RemoveMultipartFormFiles()
vv := f.Value["foo"]
if len(vv) != 1 {
t.Fatalf("unexpected values count: %d", len(vv))
}
if vv[0] != strings.Repeat("a", 8*1024) {
t.Fatalf("unexpected value length: %d", len(vv[0]))
}
})
}
func TestResponseSwapBodySerial(t *testing.T) {
t.Parallel()
+29 -3
View File
@@ -210,10 +210,14 @@ type Server struct {
// instead.
TLSConfig *tls.Config
// FormValueFunc, which is used by RequestCtx.FormValue and support for customizing
// the behaviour of the RequestCtx.FormValue function.
// FormValueFunc customizes the behavior of RequestCtx.FormValue.
//
// NetHttpFormValueFunc gives a FormValueFunc func implementation that is consistent with net/http.
// For multipart requests, the default FormValue path calls MultipartForm()
// without a body size limit. If you need a limit for multipart parsing,
// provide a custom FormValueFunc and call MultipartFormWithLimit() there.
//
// NetHttpFormValueFunc gives a FormValueFunc implementation that is
// consistent with net/http.
FormValueFunc FormValueFunc
nextProtos map[string]ServeHandler
@@ -1077,6 +1081,9 @@ func (ctx *RequestCtx) PostArgs() *Args {
// Returns ErrNoMultipartForm if request's content-type
// isn't 'multipart/form-data'.
//
// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size
// limit is applied during multipart parsing.
//
// All uploaded temporary files are automatically deleted after
// returning from RequestHandler. Either move or copy uploaded files
// into new place if you want retaining them.
@@ -1090,6 +1097,17 @@ func (ctx *RequestCtx) MultipartForm() (*multipart.Form, error) {
return ctx.Request.MultipartForm()
}
// MultipartFormWithLimit returns request's multipart form and limits the read
// multipart body size to maxBodySize bytes.
//
// If maxBodySize <= 0, then no limit is applied.
//
// Call this method before FormValue/FormFile if you need a limit for
// multipart parsing.
func (ctx *RequestCtx) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) {
return ctx.Request.MultipartFormWithLimit(maxBodySize)
}
// FormFile returns uploaded file associated with the given multipart form key.
//
// The file is automatically deleted after returning from RequestHandler,
@@ -1098,6 +1116,9 @@ func (ctx *RequestCtx) MultipartForm() (*multipart.Form, error) {
// Use SaveMultipartFile function for permanently saving uploaded file.
//
// The returned file header is valid until your request handler returns.
//
// For multipart requests with untrusted input, call MultipartFormWithLimit()
// before FormFile.
func (ctx *RequestCtx) FormFile(key string) (*multipart.FileHeader, error) {
mf, err := ctx.MultipartForm()
if err != nil {
@@ -1182,6 +1203,10 @@ func SaveMultipartFile(fh *multipart.FileHeader, path string) (err error) {
// - FormFile for obtaining uploaded files.
//
// The returned value is valid until your request handler returns.
//
// For multipart requests with untrusted input, either call
// MultipartFormWithLimit() before FormValue or provide a custom
// Server.FormValueFunc that uses MultipartFormWithLimit().
func (ctx *RequestCtx) FormValue(key string) []byte {
if ctx.formValueFunc != nil {
return ctx.formValueFunc(ctx, key)
@@ -1189,6 +1214,7 @@ func (ctx *RequestCtx) FormValue(key string) []byte {
return defaultFormValue(ctx, key)
}
// FormValueFunc customizes how RequestCtx.FormValue resolves a value.
type FormValueFunc func(*RequestCtx, string) []byte
var (
+5 -1
View File
@@ -139,12 +139,16 @@ func AppendZstdBytes(dst, src []byte) []byte {
// WriteUnzstd writes unzstd p to w and returns the number of uncompressed
// bytes written to w.
func WriteUnzstd(w io.Writer, p []byte) (int, error) {
return writeUnzstd(w, p, 0)
}
func writeUnzstd(w io.Writer, p []byte, maxBodySize int) (int, error) {
r := &byteSliceReader{b: p}
zr, err := acquireZstdReader(r)
if err != nil {
return 0, err
}
n, err := copyZeroAlloc(w, zr)
n, err := copyZeroAllocWithLimit(w, zr, maxBodySize)
releaseZstdReader(zr)
nn := int(n)
if int64(nn) != n {