300 lines
9.5 KiB
Go
300 lines
9.5 KiB
Go
package contentcoding
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"compress/gzip"
|
|
"compress/zlib"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
"github.com/klauspost/compress/zstd"
|
|
)
|
|
|
|
func TestParseContentEncoding(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
values []string
|
|
want Encoding
|
|
wantErr error
|
|
}{
|
|
{name: "missing is identity", want: Identity},
|
|
{name: "empty is identity", values: []string{" "}, want: Identity},
|
|
{name: "identity is case insensitive", values: []string{" IDENTITY "}, want: Identity},
|
|
{name: "gzip", values: []string{"gzip"}, want: Gzip},
|
|
{name: "brotli", values: []string{"BR"}, want: Brotli},
|
|
{name: "deflate", values: []string{"deflate"}, want: Deflate},
|
|
{name: "zstd", values: []string{"zstd"}, want: Zstd},
|
|
{name: "multiple fields", values: []string{"gzip", "br"}, wantErr: ErrUnsupportedContentEncoding},
|
|
{name: "stacked coding", values: []string{"gzip, br"}, wantErr: ErrUnsupportedContentEncoding},
|
|
{name: "unknown coding", values: []string{"compress"}, wantErr: ErrUnsupportedContentEncoding},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ParseContentEncoding(tt.values)
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Fatalf("ParseContentEncoding(%q) error = %v, want errors.Is(_, %v)", tt.values, err, tt.wantErr)
|
|
}
|
|
if got != tt.want {
|
|
t.Fatalf("ParseContentEncoding(%q) = %q, want %q", tt.values, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeLimited(t *testing.T) {
|
|
plain := []byte("content coding boundary")
|
|
tests := []struct {
|
|
name string
|
|
encoding Encoding
|
|
encoded []byte
|
|
}{
|
|
{name: "identity", encoding: Identity, encoded: plain},
|
|
{name: "gzip", encoding: Gzip, encoded: encodeGzip(t, plain)},
|
|
{name: "brotli", encoding: Brotli, encoded: encodeBrotli(t, plain)},
|
|
{name: "zlib deflate", encoding: Deflate, encoded: encodeZlib(t, plain)},
|
|
{name: "zstd", encoding: Zstd, encoded: encodeZstd(t, plain, ZstdHTTPMaxWindowBytes)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := DecodeLimited(tt.encoding, tt.encoded, int64(len(plain)))
|
|
if err != nil {
|
|
t.Fatalf("DecodeLimited(%q) error = %v", tt.encoding, err)
|
|
}
|
|
if !bytes.Equal(got, plain) {
|
|
t.Fatalf("DecodeLimited(%q) = %q, want %q", tt.encoding, got, plain)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeLimitedRejectsInvalidStreams(t *testing.T) {
|
|
plain := []byte("content coding boundary")
|
|
tests := []struct {
|
|
name string
|
|
encoding Encoding
|
|
encoded []byte
|
|
}{
|
|
{name: "gzip empty", encoding: Gzip},
|
|
{name: "brotli empty", encoding: Brotli},
|
|
{name: "deflate empty", encoding: Deflate},
|
|
{name: "zstd empty", encoding: Zstd},
|
|
{name: "gzip truncated", encoding: Gzip, encoded: truncate(encodeGzip(t, plain))},
|
|
{name: "brotli truncated", encoding: Brotli, encoded: truncate(encodeBrotli(t, plain))},
|
|
{name: "deflate truncated", encoding: Deflate, encoded: truncate(encodeZlib(t, plain))},
|
|
{name: "zstd truncated", encoding: Zstd, encoded: truncate(encodeZstd(t, plain, ZstdHTTPMaxWindowBytes))},
|
|
{name: "gzip corrupt", encoding: Gzip, encoded: []byte("not a gzip stream")},
|
|
{name: "brotli corrupt", encoding: Brotli, encoded: []byte("not a brotli stream")},
|
|
{name: "deflate corrupt", encoding: Deflate, encoded: []byte("not a zlib stream")},
|
|
{name: "zstd corrupt", encoding: Zstd, encoded: []byte("not a zstd stream")},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := DecodeLimited(tt.encoding, tt.encoded, 1024)
|
|
if !errors.Is(err, ErrInvalidContentEncoding) || got != nil {
|
|
t.Fatalf("DecodeLimited(%q, %q) = %q, %v; want invalid content encoding", tt.encoding, tt.encoded, got, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeLimitedHonorsDecodedLimit(t *testing.T) {
|
|
plain := []byte("limit")
|
|
tests := []struct {
|
|
name string
|
|
encoding Encoding
|
|
encoded []byte
|
|
}{
|
|
{name: "identity", encoding: Identity, encoded: plain},
|
|
{name: "gzip", encoding: Gzip, encoded: encodeGzip(t, plain)},
|
|
{name: "brotli", encoding: Brotli, encoded: encodeBrotli(t, plain)},
|
|
{name: "deflate", encoding: Deflate, encoded: encodeZlib(t, plain)},
|
|
{name: "zstd", encoding: Zstd, encoded: encodeZstd(t, plain, ZstdHTTPMaxWindowBytes)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := DecodeLimited(tt.encoding, tt.encoded, int64(len(plain)-1))
|
|
if !errors.Is(err, ErrDecodedBodyTooLarge) || got != nil {
|
|
t.Fatalf("DecodeLimited(%q) = %q, %v; want decoded body too large", tt.encoding, got, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeLimitedIdentityReturnsCopy(t *testing.T) {
|
|
encoded := []byte("identity")
|
|
got, err := DecodeLimited(Identity, encoded, int64(len(encoded)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got[0] = 'I'
|
|
if encoded[0] != 'i' {
|
|
t.Fatal("DecodeLimited(Identity) returned an aliased slice")
|
|
}
|
|
}
|
|
|
|
func TestDecodeLimitedRejectsRawDeflate(t *testing.T) {
|
|
plain := []byte("raw deflate is not HTTP deflate")
|
|
var encoded bytes.Buffer
|
|
writer, err := flate.NewWriter(&encoded, flate.DefaultCompression)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := writer.Write(plain); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := DecodeLimited(Deflate, encoded.Bytes(), 1024)
|
|
if !errors.Is(err, ErrInvalidContentEncoding) || got != nil {
|
|
t.Fatalf("DecodeLimited(raw deflate) = %q, %v; want invalid content encoding", got, err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeLimitedRejectsTrailingData(t *testing.T) {
|
|
plain := []byte("trailing bytes must not be ignored")
|
|
tests := []struct {
|
|
name string
|
|
encoding Encoding
|
|
encoded []byte
|
|
}{
|
|
{name: "gzip", encoding: Gzip, encoded: append(encodeGzip(t, plain), []byte("trailing")...)},
|
|
{name: "zlib deflate", encoding: Deflate, encoded: append(encodeZlib(t, plain), []byte("trailing")...)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := DecodeLimited(tt.encoding, tt.encoded, 1024)
|
|
if !errors.Is(err, ErrInvalidContentEncoding) || got != nil {
|
|
t.Fatalf("DecodeLimited(%q) = %q, %v; want invalid content encoding", tt.encoding, got, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeLimitedAcceptsConcatenatedGzipMembers(t *testing.T) {
|
|
first := []byte("first ")
|
|
second := []byte("second")
|
|
encoded := append(encodeGzip(t, first), encodeGzip(t, second)...)
|
|
|
|
got, err := DecodeLimited(Gzip, encoded, int64(len(first)+len(second)))
|
|
if err != nil {
|
|
t.Fatalf("DecodeLimited() error = %v", err)
|
|
}
|
|
if want := append(first, second...); !bytes.Equal(got, want) {
|
|
t.Fatalf("DecodeLimited() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestZstdHTTPWindow(t *testing.T) {
|
|
plain := []byte("small body with an explicitly declared zstd window")
|
|
t.Run("maximum window is accepted below decoded limit", func(t *testing.T) {
|
|
encoded := encodeZstdWithDeclaredWindow(t, plain, ZstdHTTPMaxWindowBytes)
|
|
got, err := DecodeLimited(Zstd, encoded, int64(len(plain)))
|
|
if err != nil || !bytes.Equal(got, plain) {
|
|
t.Fatalf("DecodeLimited() = %q, %v", got, err)
|
|
}
|
|
})
|
|
t.Run("window above maximum is rejected", func(t *testing.T) {
|
|
encoded := encodeZstdWithDeclaredWindow(t, plain, 16<<20)
|
|
got, err := DecodeLimited(Zstd, encoded, 1024)
|
|
if !errors.Is(err, ErrInvalidContentEncoding) || got != nil {
|
|
t.Fatalf("DecodeLimited() = %q, %v; want invalid content encoding", got, err)
|
|
}
|
|
})
|
|
t.Run("output limit is independent from window limit", func(t *testing.T) {
|
|
encoded := encodeZstdWithDeclaredWindow(t, plain, ZstdHTTPMaxWindowBytes)
|
|
got, err := DecodeLimited(Zstd, encoded, int64(len(plain)-1))
|
|
if !errors.Is(err, ErrDecodedBodyTooLarge) || got != nil {
|
|
t.Fatalf("DecodeLimited() = %q, %v; want decoded body too large", got, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func encodeZstdWithDeclaredWindow(t *testing.T, plain []byte, window int) []byte {
|
|
t.Helper()
|
|
encoded := encodeZstd(t, plain, window)
|
|
|
|
var header zstd.Header
|
|
if err := header.Decode(encoded); err != nil {
|
|
t.Fatalf("decode generated zstd header: %v", err)
|
|
}
|
|
header.SingleSegment = false
|
|
header.WindowSize = uint64(window)
|
|
rebuilt, err := header.AppendTo(nil)
|
|
if err != nil {
|
|
t.Fatalf("rebuild zstd header: %v", err)
|
|
}
|
|
rebuilt = append(rebuilt, encoded[header.HeaderSize:]...)
|
|
|
|
var got zstd.Header
|
|
if err := got.Decode(rebuilt); err != nil {
|
|
t.Fatalf("decode rebuilt zstd header: %v", err)
|
|
}
|
|
if got.SingleSegment || got.WindowSize != uint64(window) {
|
|
t.Fatalf("zstd fixture header = {SingleSegment:%t WindowSize:%d}, want {false %d}", got.SingleSegment, got.WindowSize, window)
|
|
}
|
|
return rebuilt
|
|
}
|
|
|
|
func encodeGzip(t *testing.T, plain []byte) []byte {
|
|
t.Helper()
|
|
var encoded bytes.Buffer
|
|
writer := gzip.NewWriter(&encoded)
|
|
writeAndClose(t, writer, plain)
|
|
return encoded.Bytes()
|
|
}
|
|
|
|
func encodeBrotli(t *testing.T, plain []byte) []byte {
|
|
t.Helper()
|
|
var encoded bytes.Buffer
|
|
writer := brotli.NewWriter(&encoded)
|
|
writeAndClose(t, writer, plain)
|
|
return encoded.Bytes()
|
|
}
|
|
|
|
func encodeZlib(t *testing.T, plain []byte) []byte {
|
|
t.Helper()
|
|
var encoded bytes.Buffer
|
|
writer := zlib.NewWriter(&encoded)
|
|
writeAndClose(t, writer, plain)
|
|
return encoded.Bytes()
|
|
}
|
|
|
|
func encodeZstd(t *testing.T, plain []byte, window int) []byte {
|
|
t.Helper()
|
|
encoder, err := zstd.NewWriter(
|
|
nil,
|
|
zstd.WithEncoderConcurrency(1),
|
|
zstd.WithSingleSegment(false),
|
|
zstd.WithWindowSize(window),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer encoder.Close()
|
|
return encoder.EncodeAll(plain, nil)
|
|
}
|
|
|
|
func writeAndClose(t *testing.T, writer io.WriteCloser, plain []byte) {
|
|
t.Helper()
|
|
if _, err := writer.Write(plain); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func truncate(encoded []byte) []byte {
|
|
return append([]byte(nil), encoded[:len(encoded)-1]...)
|
|
}
|