1082 lines
30 KiB
Go
1082 lines
30 KiB
Go
//go:build linux || freebsd
|
|
// +build linux freebsd
|
|
|
|
package compression
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"compress/lzw"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// testData is the sample data used for compression/decompression round-trip tests.
|
|
var testData = []byte("Hello, World! This is a test of the compression package. " +
|
|
"It contains various compression algorithms including gzip, flate, zlib, lzw, and bzip2. " +
|
|
"The quick brown fox jumps over the lazy dog. 1234567890!@#$%^&*()")
|
|
|
|
// ============================================================================
|
|
// BaseDecompressor Tests
|
|
// ============================================================================
|
|
|
|
func TestBaseDecompressor(t *testing.T) {
|
|
t.Run("Name", func(t *testing.T) {
|
|
bd := NewBaseDecompressor("test-name", []string{"gzip"}, []string{".gz"})
|
|
if got := bd.Name(); got != "test-name" {
|
|
t.Errorf("Name() = %q, want %q", got, "test-name")
|
|
}
|
|
})
|
|
|
|
t.Run("Extensions", func(t *testing.T) {
|
|
want := []string{".gz", ".tgz"}
|
|
bd := NewBaseDecompressor("test", []string{"gzip"}, want)
|
|
got := bd.Extensions()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("Extensions() length = %d, want %d", len(got), len(want))
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("Extensions()[%d] = %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandle", func(t *testing.T) {
|
|
bd := NewBaseDecompressor("test", []string{"gzip", "x-gzip"}, []string{".gz"})
|
|
cases := []struct {
|
|
encoding string
|
|
want bool
|
|
}{
|
|
{"gzip", true},
|
|
{"GZIP", true},
|
|
{"Gzip", true},
|
|
{"x-gzip", true},
|
|
{"X-GZIP", true},
|
|
{"x-Gzip", true},
|
|
{"deflate", false},
|
|
{"bzip2", false},
|
|
{"identity", false},
|
|
{"", false},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := bd.CanHandle(tc.encoding); got != tc.want {
|
|
t.Errorf("CanHandle(%q) = %v, want %v", tc.encoding, got, tc.want)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandle with empty encodings list", func(t *testing.T) {
|
|
bd := NewBaseDecompressor("empty", []string{}, []string{".ext"})
|
|
if bd.CanHandle("anything") {
|
|
t.Error("CanHandle('anything') = true, want false for empty encodings list")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandleFile", func(t *testing.T) {
|
|
bd := NewBaseDecompressor("test", []string{"gzip"}, []string{".gz", ".tgz"})
|
|
cases := []struct {
|
|
filename string
|
|
want bool
|
|
}{
|
|
{"file.gz", true},
|
|
{"file.GZ", true},
|
|
{"file.Gz", true},
|
|
{"archive.tar.gz", true},
|
|
{"archive.tar.tgz", true},
|
|
{".gz", true},
|
|
{"file.bz2", false},
|
|
{"file.txt", false},
|
|
{"file", false},
|
|
{"", false},
|
|
{"filegz", false}, // not a suffix
|
|
{"file.gzip", false}, // .gzip not in list
|
|
{".tgz", true},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := bd.CanHandleFile(tc.filename); got != tc.want {
|
|
t.Errorf("CanHandleFile(%q) = %v, want %v", tc.filename, got, tc.want)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandleFile with empty extensions list", func(t *testing.T) {
|
|
bd := NewBaseDecompressor("empty", []string{"gzip"}, []string{})
|
|
if bd.CanHandleFile("anything.gz") {
|
|
t.Error("CanHandleFile('anything.gz') = true, want false for empty extensions list")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Registry Tests
|
|
// ============================================================================
|
|
|
|
// testDecompressor is a minimal Decompressor implementation for registry testing.
|
|
// It embeds a BaseDecompressor so CanHandle/CanHandleFile can delegate to it.
|
|
type testDecompressor struct {
|
|
*BaseDecompressor
|
|
name string
|
|
}
|
|
|
|
func (td *testDecompressor) Name() string { return td.name }
|
|
func (td *testDecompressor) Reader(r io.Reader) (io.ReadCloser, error) {
|
|
return io.NopCloser(r), nil
|
|
}
|
|
func (td *testDecompressor) CanHandle(contentEncoding string) bool {
|
|
if td.BaseDecompressor != nil {
|
|
return td.BaseDecompressor.CanHandle(contentEncoding)
|
|
}
|
|
return false
|
|
}
|
|
func (td *testDecompressor) CanHandleFile(filename string) bool {
|
|
if td.BaseDecompressor != nil {
|
|
return td.BaseDecompressor.CanHandleFile(filename)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestRegistry(t *testing.T) {
|
|
// Save and restore the global registry to isolate this test.
|
|
savedItems := globalRegistry.items
|
|
globalRegistry.items = make(map[string]Decompressor)
|
|
defer func() { globalRegistry.items = savedItems }()
|
|
|
|
t.Run("Register and Get", func(t *testing.T) {
|
|
d := &testDecompressor{name: "mytest"}
|
|
Register(d)
|
|
|
|
got, ok := Get("mytest")
|
|
if !ok {
|
|
t.Fatal("Get('mytest') returned ok=false, want true")
|
|
}
|
|
if got.Name() != "mytest" {
|
|
t.Errorf("Get().Name() = %q, want %q", got.Name(), "mytest")
|
|
}
|
|
})
|
|
|
|
t.Run("Get is case-insensitive", func(t *testing.T) {
|
|
d := &testDecompressor{name: "case-test"}
|
|
Register(d)
|
|
|
|
for _, name := range []string{"case-test", "CASE-TEST", "Case-Test"} {
|
|
got, ok := Get(name)
|
|
if !ok {
|
|
t.Errorf("Get(%q) returned ok=false, want true", name)
|
|
continue
|
|
}
|
|
if got.Name() != "case-test" {
|
|
t.Errorf("Get(%q).Name() = %q, want %q", name, got.Name(), "case-test")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Get non-existent", func(t *testing.T) {
|
|
_, ok := Get("does-not-exist")
|
|
if ok {
|
|
t.Error("Get('does-not-exist') returned ok=true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("Get empty string", func(t *testing.T) {
|
|
_, ok := Get("")
|
|
if ok {
|
|
t.Error("Get('') returned ok=true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("Register overwrites existing", func(t *testing.T) {
|
|
d1 := &testDecompressor{name: "overwrite-me"}
|
|
d2 := &testDecompressor{name: "overwrite-me"}
|
|
Register(d1)
|
|
Register(d2)
|
|
|
|
got, ok := Get("overwrite-me")
|
|
if !ok {
|
|
t.Fatal("Get('overwrite-me') returned ok=false, want true")
|
|
}
|
|
// d2 should have overwritten d1
|
|
if got != d2 {
|
|
t.Error("Register did not overwrite the existing entry")
|
|
}
|
|
})
|
|
|
|
t.Run("GetByFilename", func(t *testing.T) {
|
|
// Register a decompressor that maps to a known filename detection result.
|
|
fd := NewBaseDecompressor("gzip", []string{"gzip"}, []string{".gz"})
|
|
Register(&testDecompressor{BaseDecompressor: fd, name: "gzip"})
|
|
|
|
d, ok := GetByFilename("archive.tar.gz")
|
|
if !ok {
|
|
t.Fatal("GetByFilename('archive.tar.gz') returned ok=false, want true")
|
|
}
|
|
if d.Name() != "gzip" {
|
|
t.Errorf("GetByFilename().Name() = %q, want %q", d.Name(), "gzip")
|
|
}
|
|
})
|
|
|
|
t.Run("GetByFilename with identity (no match)", func(t *testing.T) {
|
|
_, ok := GetByFilename("plain.txt")
|
|
if ok {
|
|
t.Error("GetByFilename('plain.txt') returned ok=true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("GetByFilename with empty string", func(t *testing.T) {
|
|
_, ok := GetByFilename("")
|
|
if ok {
|
|
t.Error("GetByFilename('') returned ok=true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("GetByFilename with unknown format", func(t *testing.T) {
|
|
_, ok := GetByFilename("file.unknown")
|
|
if ok {
|
|
t.Error("GetByFilename('file.unknown') returned ok=true, want false")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filename Detection Tests
|
|
// ============================================================================
|
|
|
|
func TestDetectCompressionByFilename(t *testing.T) {
|
|
cases := []struct {
|
|
filename string
|
|
want string
|
|
}{
|
|
// Gzip
|
|
{"file.gz", "gzip"},
|
|
{"archive.tar.gz", "gzip"},
|
|
{"file.GZ", "gzip"},
|
|
{"file.Gz", "gzip"},
|
|
{".gz", "gzip"},
|
|
{"/path/to/file.gz", "gzip"},
|
|
// Zlib
|
|
{"file.zlib", "zlib"},
|
|
{"file.Zlib", "zlib"},
|
|
{"file.ZLIB", "zlib"},
|
|
{"file.zz", "zlib"},
|
|
{"file.ZZ", "zlib"},
|
|
{".zlib", "zlib"},
|
|
{".zz", "zlib"},
|
|
// Flate / Deflate
|
|
{"file.deflate", "flate"},
|
|
{"file.DEFLATE", "flate"},
|
|
{"file.Deflate", "flate"},
|
|
{"file.fl", "flate"},
|
|
{"file.FL", "flate"},
|
|
{".deflate", "flate"},
|
|
{".fl", "flate"},
|
|
// Bzip2
|
|
{"file.bz2", "bzip2"},
|
|
{"file.BZ2", "bzip2"},
|
|
{"file.Bz2", "bzip2"},
|
|
{"file.tbz", "bzip2"},
|
|
{"file.TBZ", "bzip2"},
|
|
{"file.tbz2", "bzip2"},
|
|
{"file.TBZ2", "bzip2"},
|
|
{".bz2", "bzip2"},
|
|
{".tbz", "bzip2"},
|
|
{".tbz2", "bzip2"},
|
|
// LZW
|
|
{"file.Z", "lzw"},
|
|
{"file.lzw", "lzw"},
|
|
{"file.LZW", "lzw"},
|
|
{".Z", "lzw"},
|
|
{".lzw", "lzw"},
|
|
// Identity (no compression detected)
|
|
{"file.txt", "identity"},
|
|
{"file.tar", "identity"},
|
|
{"file", "identity"},
|
|
{"", "identity"},
|
|
{".", "identity"},
|
|
// Edge cases - false positives
|
|
{"file.gzip", "identity"}, // .gzip is not a registered extension
|
|
{"file.gz.txt", "identity"}, // .gz is not at the end
|
|
{"file.bz2.backup", "identity"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := DetectCompressionByFilename(tc.filename); got != tc.want {
|
|
t.Errorf("DetectCompressionByFilename(%q) = %q, want %q", tc.filename, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Content-Encoding Parsing Tests
|
|
// ============================================================================
|
|
|
|
func TestParseContentEncoding(t *testing.T) {
|
|
cases := []struct {
|
|
header string
|
|
want []string
|
|
}{
|
|
{"gzip", []string{"gzip"}},
|
|
{"deflate", []string{"deflate"}},
|
|
{"compress", []string{"compress"}},
|
|
{"identity", []string{"identity"}},
|
|
{"gzip, deflate", []string{"gzip", "deflate"}},
|
|
{" gzip , deflate ", []string{"gzip", "deflate"}},
|
|
{"gzip,deflate,identity", []string{"gzip", "deflate", "identity"}},
|
|
{" gzip ", []string{"gzip"}},
|
|
{"", []string{"identity"}},
|
|
{" ", []string{"identity"}},
|
|
{",,,", []string{"identity"}},
|
|
{"gzip,,deflate", []string{"gzip", "deflate"}},
|
|
}
|
|
for _, tc := range cases {
|
|
got := ParseContentEncoding(tc.header)
|
|
if !stringSliceEqual(got, tc.want) {
|
|
t.Errorf("ParseContentEncoding(%q) = %v, want %v", tc.header, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetEffectiveEncoding(t *testing.T) {
|
|
cases := []struct {
|
|
encodings []string
|
|
want string
|
|
}{
|
|
{[]string{"gzip"}, "gzip"},
|
|
{[]string{"deflate"}, "deflate"},
|
|
{[]string{"identity"}, "identity"},
|
|
{[]string{"gzip", "identity"}, "gzip"},
|
|
{[]string{"identity", "gzip"}, "gzip"},
|
|
{[]string{"identity", "identity"}, "identity"},
|
|
{[]string{"identity", "gzip", "deflate"}, "gzip"},
|
|
{[]string{}, "identity"},
|
|
{nil, "identity"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := GetEffectiveEncoding(tc.encodings); got != tc.want {
|
|
t.Errorf("GetEffectiveEncoding(%v) = %q, want %q", tc.encodings, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetCompressionFromHeader(t *testing.T) {
|
|
cases := []struct {
|
|
header string
|
|
want string
|
|
}{
|
|
{"gzip", "gzip"},
|
|
{"deflate", "deflate"},
|
|
{"compress", "compress"},
|
|
{"identity", "identity"},
|
|
{"", "identity"},
|
|
{"gzip, identity", "gzip"},
|
|
{"identity, gzip", "gzip"},
|
|
{"identity, identity", "identity"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := GetCompressionFromHeader(tc.header); got != tc.want {
|
|
t.Errorf("GetCompressionFromHeader(%q) = %q, want %q", tc.header, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsCompressedFile(t *testing.T) {
|
|
cases := []struct {
|
|
filename string
|
|
want bool
|
|
}{
|
|
// Compressed
|
|
{"file.gz", true},
|
|
{"file.bz2", true},
|
|
{"file.tbz", true},
|
|
{"file.tbz2", true},
|
|
{"file.zlib", true},
|
|
{"file.zz", true},
|
|
{"file.deflate", true},
|
|
{"file.fl", true},
|
|
{"file.Z", true},
|
|
{"file.lzw", true},
|
|
{".gz", true},
|
|
// Not compressed
|
|
{"file.txt", false},
|
|
{"file.tar", false},
|
|
{"file", false},
|
|
{"", false},
|
|
{".", false},
|
|
{"file.gzip", false},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := IsCompressedFile(tc.filename); got != tc.want {
|
|
t.Errorf("IsCompressedFile(%q) = %v, want %v", tc.filename, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Gzip Round-trip Test
|
|
// ============================================================================
|
|
|
|
func TestGzipDecompressor(t *testing.T) {
|
|
t.Run("Name", func(t *testing.T) {
|
|
d := NewGzipDecompressor()
|
|
if got := d.Name(); got != "gzip" {
|
|
t.Errorf("Name() = %q, want %q", got, "gzip")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandle", func(t *testing.T) {
|
|
d := NewGzipDecompressor()
|
|
if !d.CanHandle("gzip") {
|
|
t.Error("CanHandle('gzip') = false, want true")
|
|
}
|
|
if d.CanHandle("deflate") {
|
|
t.Error("CanHandle('deflate') = true, want false")
|
|
}
|
|
if d.CanHandle("") {
|
|
t.Error("CanHandle('') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandleFile", func(t *testing.T) {
|
|
d := NewGzipDecompressor()
|
|
if !d.CanHandleFile("file.gz") {
|
|
t.Error("CanHandleFile('file.gz') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.GZ") {
|
|
t.Error("CanHandleFile('file.GZ') = false, want true")
|
|
}
|
|
if d.CanHandleFile("file.bz2") {
|
|
t.Error("CanHandleFile('file.bz2') = true, want false")
|
|
}
|
|
if d.CanHandleFile("file") {
|
|
t.Error("CanHandleFile('file') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("RoundTrip compress/decompress", func(t *testing.T) {
|
|
var compressed bytes.Buffer
|
|
|
|
// Compress
|
|
c := NewGzipCompressor()
|
|
writer, err := c.Writer(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("GzipCompressor.Writer() error = %v", err)
|
|
}
|
|
n, err := writer.Write(testData)
|
|
if err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
if n != len(testData) {
|
|
t.Errorf("Write() wrote %d bytes, want %d", n, len(testData))
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
|
|
// Verify compression happened (sanity check)
|
|
if compressed.Len() == 0 {
|
|
t.Fatal("Compressed output is empty")
|
|
}
|
|
|
|
// Decompress
|
|
d := NewGzipDecompressor()
|
|
reader, err := d.Reader(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("GzipDecompressor.Reader() error = %v", err)
|
|
}
|
|
decompressed, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll() error = %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(decompressed, testData) {
|
|
t.Fatalf("Decompressed data mismatch:\ngot: %q\nwant: %q",
|
|
string(decompressed), string(testData))
|
|
}
|
|
})
|
|
|
|
t.Run("Decompress with invalid data", func(t *testing.T) {
|
|
d := NewGzipDecompressor()
|
|
_, err := d.Reader(strings.NewReader("not gzip data"))
|
|
if err == nil {
|
|
t.Error("GzipDecompressor.Reader() with invalid data expected error, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("Compressor Name", func(t *testing.T) {
|
|
c := NewGzipCompressor()
|
|
if got := c.Name(); got != "gzip" {
|
|
t.Errorf("Compressor Name() = %q, want %q", got, "gzip")
|
|
}
|
|
})
|
|
|
|
t.Run("Init registers decompressor", func(t *testing.T) {
|
|
d, ok := Get("gzip")
|
|
if !ok {
|
|
t.Fatal("Get('gzip') returned ok=false after init, want true")
|
|
}
|
|
if d.Name() != "gzip" {
|
|
t.Errorf("Get('gzip').Name() = %q, want %q", d.Name(), "gzip")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Flate Round-trip Test
|
|
// ============================================================================
|
|
|
|
func TestFlateDecompressor(t *testing.T) {
|
|
t.Run("Name", func(t *testing.T) {
|
|
d := NewFlateDecompressor()
|
|
if got := d.Name(); got != "flate" {
|
|
t.Errorf("Name() = %q, want %q", got, "flate")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandleFile", func(t *testing.T) {
|
|
d := NewFlateDecompressor()
|
|
if !d.CanHandleFile("file.deflate") {
|
|
t.Error("CanHandleFile('file.deflate') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.fl") {
|
|
t.Error("CanHandleFile('file.fl') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.DEFLATE") {
|
|
t.Error("CanHandleFile('file.DEFLATE') = false, want true")
|
|
}
|
|
if d.CanHandleFile("file.gz") {
|
|
t.Error("CanHandleFile('file.gz') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("RoundTrip compress/decompress default level", func(t *testing.T) {
|
|
compressAndDecompressFlate(t, flate.DefaultCompression)
|
|
})
|
|
|
|
t.Run("RoundTrip with NoCompression", func(t *testing.T) {
|
|
compressAndDecompressFlate(t, flate.NoCompression)
|
|
})
|
|
|
|
t.Run("RoundTrip with BestSpeed", func(t *testing.T) {
|
|
compressAndDecompressFlate(t, flate.BestSpeed)
|
|
})
|
|
|
|
t.Run("RoundTrip with BestCompression", func(t *testing.T) {
|
|
compressAndDecompressFlate(t, flate.BestCompression)
|
|
})
|
|
|
|
t.Run("RoundTrip with HuffmanOnly", func(t *testing.T) {
|
|
compressAndDecompressFlate(t, flate.HuffmanOnly)
|
|
})
|
|
|
|
t.Run("Invalid compression level - too low", func(t *testing.T) {
|
|
_, err := NewFlateCompressorWithLevel(-3)
|
|
if err == nil {
|
|
t.Error("NewFlateCompressorWithLevel(-3) expected error, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid compression level - too high", func(t *testing.T) {
|
|
_, err := NewFlateCompressorWithLevel(10)
|
|
if err == nil {
|
|
t.Error("NewFlateCompressorWithLevel(10) expected error, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("Boundary valid levels", func(t *testing.T) {
|
|
// flate.NoCompression = -1, flate.BestCompression = 9
|
|
_, errLow := NewFlateCompressorWithLevel(flate.NoCompression)
|
|
if errLow != nil {
|
|
t.Errorf("NewFlateCompressorWithLevel(%d) unexpected error: %v", flate.NoCompression, errLow)
|
|
}
|
|
_, errHigh := NewFlateCompressorWithLevel(flate.BestCompression)
|
|
if errHigh != nil {
|
|
t.Errorf("NewFlateCompressorWithLevel(%d) unexpected error: %v", flate.BestCompression, errHigh)
|
|
}
|
|
})
|
|
|
|
t.Run("Compressor Name", func(t *testing.T) {
|
|
c := NewFlateCompressor()
|
|
if got := c.Name(); got != "flate" {
|
|
t.Errorf("Compressor Name() = %q, want %q", got, "flate")
|
|
}
|
|
})
|
|
|
|
t.Run("Init registers decompressor", func(t *testing.T) {
|
|
d, ok := Get("flate")
|
|
if !ok {
|
|
t.Fatal("Get('flate') returned ok=false after init, want true")
|
|
}
|
|
if d.Name() != "flate" {
|
|
t.Errorf("Get('flate').Name() = %q, want %q", d.Name(), "flate")
|
|
}
|
|
})
|
|
}
|
|
|
|
// compressAndDecompressFlate is a helper for flate round-trip tests.
|
|
func compressAndDecompressFlate(t *testing.T, level int) {
|
|
t.Helper()
|
|
|
|
var compressed bytes.Buffer
|
|
|
|
c, err := NewFlateCompressorWithLevel(level)
|
|
if err != nil {
|
|
t.Fatalf("NewFlateCompressorWithLevel(%d) error = %v", level, err)
|
|
}
|
|
writer, err := c.Writer(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("Writer() error = %v", err)
|
|
}
|
|
n, err := writer.Write(testData)
|
|
if err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
if n != len(testData) {
|
|
t.Errorf("Write() wrote %d bytes, want %d", n, len(testData))
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
|
|
d := NewFlateDecompressor()
|
|
reader, err := d.Reader(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("FlateDecompressor.Reader() error = %v", err)
|
|
}
|
|
decompressed, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll() error = %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(decompressed, testData) {
|
|
t.Fatalf("Level %d: decompressed data mismatch:\ngot: %q\nwant: %q",
|
|
level, string(decompressed), string(testData))
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Zlib Round-trip Test
|
|
// ============================================================================
|
|
|
|
func TestZlibDecompressor(t *testing.T) {
|
|
t.Run("Name", func(t *testing.T) {
|
|
d := NewZlibDecompressor()
|
|
if got := d.Name(); got != "zlib" {
|
|
t.Errorf("Name() = %q, want %q", got, "zlib")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandle deflate (HTTP Content-Encoding)", func(t *testing.T) {
|
|
d := NewZlibDecompressor()
|
|
if !d.CanHandle("deflate") {
|
|
t.Error("CanHandle('deflate') = false, want true (zlib handles 'deflate' encoding)")
|
|
}
|
|
if d.CanHandle("gzip") {
|
|
t.Error("CanHandle('gzip') = true, want false")
|
|
}
|
|
if d.CanHandle("") {
|
|
t.Error("CanHandle('') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandleFile", func(t *testing.T) {
|
|
d := NewZlibDecompressor()
|
|
if !d.CanHandleFile("file.zlib") {
|
|
t.Error("CanHandleFile('file.zlib') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.zz") {
|
|
t.Error("CanHandleFile('file.zz') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.ZLIB") {
|
|
t.Error("CanHandleFile('file.ZLIB') = false, want true")
|
|
}
|
|
if d.CanHandleFile("file.gz") {
|
|
t.Error("CanHandleFile('file.gz') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("RoundTrip compress/decompress", func(t *testing.T) {
|
|
var compressed bytes.Buffer
|
|
|
|
c := NewZlibCompressor()
|
|
writer, err := c.Writer(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("ZlibCompressor.Writer() error = %v", err)
|
|
}
|
|
n, err := writer.Write(testData)
|
|
if err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
if n != len(testData) {
|
|
t.Errorf("Write() wrote %d bytes, want %d", n, len(testData))
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
|
|
d := NewZlibDecompressor()
|
|
reader, err := d.Reader(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("ZlibDecompressor.Reader() error = %v", err)
|
|
}
|
|
decompressed, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll() error = %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(decompressed, testData) {
|
|
t.Fatalf("Decompressed data mismatch:\ngot: %q\nwant: %q",
|
|
string(decompressed), string(testData))
|
|
}
|
|
})
|
|
|
|
t.Run("Decompress with invalid data", func(t *testing.T) {
|
|
d := NewZlibDecompressor()
|
|
_, err := d.Reader(strings.NewReader("not zlib data"))
|
|
if err == nil {
|
|
t.Error("ZlibDecompressor.Reader() with invalid data expected error, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("Compressor Name", func(t *testing.T) {
|
|
c := NewZlibCompressor()
|
|
if got := c.Name(); got != "zlib" {
|
|
t.Errorf("Compressor Name() = %q, want %q", got, "zlib")
|
|
}
|
|
})
|
|
|
|
t.Run("Init registers decompressor", func(t *testing.T) {
|
|
d, ok := Get("zlib")
|
|
if !ok {
|
|
t.Fatal("Get('zlib') returned ok=false after init, want true")
|
|
}
|
|
if d.Name() != "zlib" {
|
|
t.Errorf("Get('zlib').Name() = %q, want %q", d.Name(), "zlib")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// LZW Round-trip Test
|
|
// ============================================================================
|
|
|
|
func TestLzwDecompressor(t *testing.T) {
|
|
t.Run("Name with LSB (default)", func(t *testing.T) {
|
|
d := NewLzwDecompressor()
|
|
if got := d.Name(); got != "lzw" {
|
|
t.Errorf("Name() = %q, want %q", got, "lzw")
|
|
}
|
|
})
|
|
|
|
t.Run("Name with MSB", func(t *testing.T) {
|
|
d := NewLzwDecompressorWithOrder(lzw.MSB)
|
|
if got := d.Name(); got != "lzw-msb" {
|
|
t.Errorf("Name() with MSB = %q, want %q", got, "lzw-msb")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandleFile", func(t *testing.T) {
|
|
d := NewLzwDecompressor()
|
|
if !d.CanHandleFile("file.Z") {
|
|
t.Error("CanHandleFile('file.Z') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.lzw") {
|
|
t.Error("CanHandleFile('file.lzw') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.LZW") {
|
|
t.Error("CanHandleFile('file.LZW') = false, want true")
|
|
}
|
|
if d.CanHandleFile("file.gz") {
|
|
t.Error("CanHandleFile('file.gz') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("RoundTrip LSB (default)", func(t *testing.T) {
|
|
var compressed bytes.Buffer
|
|
|
|
c := NewLzwCompressor() // LSB by default
|
|
writer, err := c.Writer(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("LzwCompressor.Writer() error = %v", err)
|
|
}
|
|
n, err := writer.Write(testData)
|
|
if err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
if n != len(testData) {
|
|
t.Errorf("Write() wrote %d bytes, want %d", n, len(testData))
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
|
|
d := NewLzwDecompressor() // LSB by default
|
|
reader, err := d.Reader(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("LzwDecompressor.Reader() error = %v", err)
|
|
}
|
|
decompressed, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll() error = %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(decompressed, testData) {
|
|
t.Fatalf("Decompressed data mismatch:\ngot: %q\nwant: %q",
|
|
string(decompressed), string(testData))
|
|
}
|
|
})
|
|
|
|
t.Run("RoundTrip MSB", func(t *testing.T) {
|
|
var compressed bytes.Buffer
|
|
|
|
c := NewLzwCompressorWithOrder(lzw.MSB)
|
|
writer, err := c.Writer(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("LzwCompressor(MSB).Writer() error = %v", err)
|
|
}
|
|
_, err = writer.Write(testData)
|
|
if err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
|
|
d := NewLzwDecompressorWithOrder(lzw.MSB)
|
|
reader, err := d.Reader(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("LzwDecompressor(MSB).Reader() error = %v", err)
|
|
}
|
|
decompressed, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll() error = %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(decompressed, testData) {
|
|
t.Fatalf("Decompressed data mismatch:\ngot: %q\nwant: %q",
|
|
string(decompressed), string(testData))
|
|
}
|
|
})
|
|
|
|
t.Run("Decompress LSB with MSB reader fails", func(t *testing.T) {
|
|
var compressed bytes.Buffer
|
|
|
|
c := NewLzwCompressorWithOrder(lzw.MSB)
|
|
writer, _ := c.Writer(&compressed)
|
|
writer.Write(testData)
|
|
writer.Close()
|
|
|
|
d := NewLzwDecompressor() // LSB reader
|
|
reader, err := d.Reader(&compressed)
|
|
if err != nil {
|
|
t.Fatalf("LzwDecompressor.Reader() unexpected error = %v", err)
|
|
}
|
|
_, err = io.ReadAll(reader)
|
|
// This should fail because we compressed with MSB but decompress with LSB
|
|
if err == nil {
|
|
t.Log("Note: LZW MSB-compressed data with LSB reader did not error (may produce garbage)")
|
|
}
|
|
})
|
|
|
|
t.Run("Compressor Name LSB", func(t *testing.T) {
|
|
c := NewLzwCompressor()
|
|
if got := c.Name(); got != "lzw" {
|
|
t.Errorf("Compressor Name() = %q, want %q", got, "lzw")
|
|
}
|
|
})
|
|
|
|
t.Run("Compressor Name MSB", func(t *testing.T) {
|
|
c := NewLzwCompressorWithOrder(lzw.MSB)
|
|
if got := c.Name(); got != "lzw-msb" {
|
|
t.Errorf("Compressor Name() with MSB = %q, want %q", got, "lzw-msb")
|
|
}
|
|
})
|
|
|
|
t.Run("Init registers decompressor", func(t *testing.T) {
|
|
d, ok := Get("lzw")
|
|
if !ok {
|
|
t.Fatal("Get('lzw') returned ok=false after init, want true")
|
|
}
|
|
if d.Name() != "lzw" {
|
|
t.Errorf("Get('lzw').Name() = %q, want %q", d.Name(), "lzw")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Bzip2 Decompressor Tests
|
|
// ============================================================================
|
|
|
|
func TestBzip2Decompressor(t *testing.T) {
|
|
t.Run("NewBzip2Decompressor returns non-nil", func(t *testing.T) {
|
|
d := NewBzip2Decompressor()
|
|
if d == nil {
|
|
t.Fatal("NewBzip2Decompressor() returned nil")
|
|
}
|
|
})
|
|
|
|
t.Run("Name", func(t *testing.T) {
|
|
d := NewBzip2Decompressor()
|
|
if got := d.Name(); got != "bzip2" {
|
|
t.Errorf("Name() = %q, want %q", got, "bzip2")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandle", func(t *testing.T) {
|
|
d := NewBzip2Decompressor()
|
|
if !d.CanHandle("bzip2") {
|
|
t.Error("CanHandle('bzip2') = false, want true")
|
|
}
|
|
if !d.CanHandle("BZIP2") {
|
|
t.Error("CanHandle('BZIP2') = false, want true")
|
|
}
|
|
if !d.CanHandle("Bzip2") {
|
|
t.Error("CanHandle('Bzip2') = false, want true")
|
|
}
|
|
if d.CanHandle("gzip") {
|
|
t.Error("CanHandle('gzip') = true, want false")
|
|
}
|
|
if d.CanHandle("") {
|
|
t.Error("CanHandle('') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("CanHandleFile", func(t *testing.T) {
|
|
d := NewBzip2Decompressor()
|
|
if !d.CanHandleFile("file.bz2") {
|
|
t.Error("CanHandleFile('file.bz2') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.tbz") {
|
|
t.Error("CanHandleFile('file.tbz') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.tbz2") {
|
|
t.Error("CanHandleFile('file.tbz2') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.BZ2") {
|
|
t.Error("CanHandleFile('file.BZ2') = false, want true")
|
|
}
|
|
if !d.CanHandleFile("file.TBZ") {
|
|
t.Error("CanHandleFile('file.TBZ') = false, want true")
|
|
}
|
|
if d.CanHandleFile("file.gz") {
|
|
t.Error("CanHandleFile('file.gz') = true, want false")
|
|
}
|
|
if d.CanHandleFile("file") {
|
|
t.Error("CanHandleFile('file') = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("Reader returns a valid io.Reader for any input", func(t *testing.T) {
|
|
// bzip2.NewReader wraps the reader lazily and never returns an error.
|
|
// It will only error on the first Read() call if the data is invalid.
|
|
d := NewBzip2Decompressor()
|
|
reader, err := d.Reader(strings.NewReader("not real bzip2 data"))
|
|
if err != nil {
|
|
t.Fatalf("Bzip2Decompressor.Reader() unexpected error = %v", err)
|
|
}
|
|
if reader == nil {
|
|
t.Fatal("Bzip2Decompressor.Reader() returned nil reader")
|
|
}
|
|
})
|
|
|
|
t.Run("Compressor Name", func(t *testing.T) {
|
|
c := NewBzip2Compressor()
|
|
if got := c.Name(); got != "bzip2" {
|
|
t.Errorf("Compressor Name() = %q, want %q", got, "bzip2")
|
|
}
|
|
})
|
|
|
|
t.Run("Compressor Writer returns error (not in stdlib)", func(t *testing.T) {
|
|
c := NewBzip2Compressor()
|
|
var buf bytes.Buffer
|
|
_, err := c.Writer(&buf)
|
|
if err == nil {
|
|
t.Error("Bzip2Compressor.Writer() expected error, got nil")
|
|
}
|
|
if err.Error() == "" {
|
|
t.Error("Bzip2Compressor.Writer() error message should not be empty")
|
|
}
|
|
})
|
|
|
|
t.Run("Init registers decompressor", func(t *testing.T) {
|
|
d, ok := Get("bzip2")
|
|
if !ok {
|
|
t.Fatal("Get('bzip2') returned ok=false after init, want true")
|
|
}
|
|
if d.Name() != "bzip2" {
|
|
t.Errorf("Get('bzip2').Name() = %q, want %q", d.Name(), "bzip2")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
// stringSliceEqual reports whether two string slices are equal.
|
|
func stringSliceEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TestDecompressorReaderIsClosable is a regression guard for the BACKLOG
|
|
// entry "Gzip/zlib/flate/lzw Close() never called on decompressor". The
|
|
// previous Decompressor.Reader interface returned io.Reader, so the
|
|
// Close method on *gzip.Reader, *zlib.Reader, *flate.Reader and
|
|
// *lzw.Reader was lost at the API boundary. Callers could not release
|
|
// the decompressor's internal state, so a long-lived mirror scan over
|
|
// many .gz assets would accumulate decoder tables without bound.
|
|
//
|
|
// Verify that the new interface (io.ReadCloser) is honoured: each
|
|
// decompressor's reader exposes a working Close, calling it after
|
|
// reading must not panic, and double-close must also be safe.
|
|
func TestDecompressorReaderIsClosable(t *testing.T) {
|
|
decs := []Decompressor{
|
|
NewGzipDecompressor(),
|
|
NewZlibDecompressor(),
|
|
NewFlateDecompressor(),
|
|
NewLzwDecompressor(),
|
|
NewBzip2Decompressor(),
|
|
}
|
|
|
|
// Each decompressor is given the *identity* input (just enough to
|
|
// satisfy NewReader for the formats that need a header — gzip, zlib,
|
|
// lzw, bzip2). We only care about Close behaviour, not actual
|
|
// decompression output, so identity input is enough for the
|
|
// constructor; we never call Read.
|
|
for _, dec := range decs {
|
|
t.Run(dec.Name(), func(t *testing.T) {
|
|
reader, err := dec.Reader(strings.NewReader(""))
|
|
if err != nil {
|
|
// NewReader on identity input may fail for some formats
|
|
// (e.g. gzip requires a valid header). That's fine — the
|
|
// test only asserts that, *whenever* we get a reader
|
|
// back, it is safely Close-able.
|
|
t.Logf("Reader returned err=%v (acceptable for empty input); skipping close check", err)
|
|
return
|
|
}
|
|
if reader == nil {
|
|
t.Fatal("Reader returned nil reader")
|
|
}
|
|
// First Close: must not panic.
|
|
if err := reader.Close(); err != nil {
|
|
t.Errorf("first Close returned err=%v, want nil", err)
|
|
}
|
|
// Second Close: many stdlib readers return nil or a
|
|
// documented sentinel on second close; we just require no
|
|
// panic.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("second Close panicked: %v", r)
|
|
}
|
|
}()
|
|
_ = reader.Close()
|
|
})
|
|
}
|
|
}
|