//go:build linux || freebsd // +build linux freebsd package compression import ( "io" "strings" "sync" ) // Decompressor defines the interface for decompression type Decompressor interface { Name() string CanHandle(contentEncoding string) bool CanHandleFile(filename string) bool // Reader returns an io.ReadCloser (not just an io.Reader) so // callers can release the decompressor's internal state — decoder // tables, dictionaries, etc. — when finished. Forgetting to call // Close on a long-lived mirror scan would let decoder state // accumulate without bound. Reader(r io.Reader) (io.ReadCloser, error) } // Compressor defines the interface for compression type Compressor interface { Name() string Writer(w io.Writer) (io.WriteCloser, error) } // BaseDecompressor is the base implementation type BaseDecompressor struct { name string contentEncodings []string fileExtensions []string } func NewBaseDecompressor(name string, contentEncodings, fileExtensions []string) *BaseDecompressor { return &BaseDecompressor{ name: name, contentEncodings: contentEncodings, fileExtensions: fileExtensions, } } func (bd *BaseDecompressor) Name() string { return bd.name } func (bd *BaseDecompressor) Extensions() []string { return bd.fileExtensions } func (bd *BaseDecompressor) CanHandle(contentEncoding string) bool { for _, e := range bd.contentEncodings { if strings.EqualFold(e, contentEncoding) { return true } } return false } func (bd *BaseDecompressor) CanHandleFile(filename string) bool { filename = strings.ToLower(filename) for _, e := range bd.fileExtensions { if strings.HasSuffix(filename, e) { return true } } return false } // Registry for decompressors type registry struct { mu sync.RWMutex items map[string]Decompressor } var globalRegistry = ®istry{items: make(map[string]Decompressor)} func Register(d Decompressor) { globalRegistry.mu.Lock() defer globalRegistry.mu.Unlock() globalRegistry.items[strings.ToLower(d.Name())] = d } func Get(name string) (Decompressor, bool) { globalRegistry.mu.RLock() defer globalRegistry.mu.RUnlock() d, ok := globalRegistry.items[strings.ToLower(name)] return d, ok } func GetByFilename(filename string) (Decompressor, bool) { format := DetectCompressionByFilename(filename) if format == "" || format == "identity" { return nil, false } return Get(format) } // GetCompressionFromHeader detects compression from the Content-Encoding header func GetCompressionFromHeader(contentEncoding string) string { if contentEncoding == "" { return "identity" } encodings := ParseContentEncoding(contentEncoding) return GetEffectiveEncoding(encodings) } func ParseContentEncoding(header string) []string { if header == "" { return []string{"identity"} } parts := strings.Split(header, ",") result := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { result = append(result, strings.ToLower(p)) } } if len(result) == 0 { return []string{"identity"} } return result } func GetEffectiveEncoding(encodings []string) string { for _, e := range encodings { if e != "identity" { return e } } return "identity" } func DetectCompressionByFilename(filename string) string { filename = strings.ToLower(filename) switch { case strings.HasSuffix(filename, ".gz"): return "gzip" case strings.HasSuffix(filename, ".zlib"), strings.HasSuffix(filename, ".zz"): return "zlib" case strings.HasSuffix(filename, ".deflate"), strings.HasSuffix(filename, ".fl"): return "flate" case strings.HasSuffix(filename, ".bz2"), strings.HasSuffix(filename, ".tbz"), strings.HasSuffix(filename, ".tbz2"): return "bzip2" case strings.HasSuffix(filename, ".z"), strings.HasSuffix(filename, ".lzw"): return "lzw" } return "identity" } func IsCompressedFile(filename string) bool { return DetectCompressionByFilename(filename) != "identity" }