Files
goget/internal/compression/compression_test.go
T

1082 lines
30 KiB
Go
Raw Normal View History

//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()
})
}
}