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