151 lines
3.9 KiB
Go
151 lines
3.9 KiB
Go
//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"
|
||
|
|
}
|