// Файл: internal/compression/compression.go // Назначение: Реализация сжатия данных с использованием различных алгоритмов. // Поддерживаемые алгоритмы: Snappy (по умолчанию), LZ4, Zstandard. // Обеспечивает прозрачное сжатие/распаковку для документов. package compression import ( "bytes" "encoding/binary" "fmt" "github.com/golang/snappy" "github.com/klauspost/compress/zstd" "github.com/pierrec/lz4/v4" ) // Config представляет конфигурацию сжатия type Config struct { Enabled bool // Включено ли сжатие Algorithm string // Алгоритм сжатия: snappy, lz4, zstd Level int // Уровень сжатия (1-9) MinSize int // Минимальный размер для сжатия (байт) } // MagicNumber используется для идентификации сжатых данных var MagicNumber = []byte{0x46, 0x54, 0x52, 0x53} // "FTRS" - Futriis // CompressionType определяет тип сжатия type CompressionType byte const ( CompressionNone CompressionType = 0x00 CompressionSnappy CompressionType = 0x01 CompressionLZ4 CompressionType = 0x02 CompressionZstd CompressionType = 0x03 ) // Compress сжимает данные с использованием указанного алгоритма func Compress(data []byte, config *Config) ([]byte, error) { if !config.Enabled { return data, nil } if len(data) < config.MinSize { return data, nil } var compressed []byte var err error var compType CompressionType switch config.Algorithm { case "snappy": compressed = snappy.Encode(nil, data) compType = CompressionSnappy case "lz4": buf := bytes.NewBuffer(nil) lz4Writer := lz4.NewWriter(buf) // Установка уровня сжатия для LZ4 if config.Level > 0 { // LZ4 уровни: 0-9, где 0=быстрый, 9=максимальное сжатие compressionLevel := lz4.CompressionLevel(config.Level) if err := lz4Writer.Apply(lz4.CompressionLevelOption(compressionLevel)); err != nil { return nil, fmt.Errorf("failed to set LZ4 compression level: %v", err) } } if _, err := lz4Writer.Write(data); err != nil { return nil, err } if err := lz4Writer.Close(); err != nil { return nil, err } compressed = buf.Bytes() compType = CompressionLZ4 case "zstd": // Для Zstandard используем предустановленные уровни скорости var encoder *zstd.Encoder var encoderLevel zstd.EncoderLevel // Выбираем уровень сжатия на основе config.Level switch { case config.Level <= 1: encoderLevel = zstd.SpeedFastest case config.Level <= 3: encoderLevel = zstd.SpeedDefault case config.Level <= 6: encoderLevel = zstd.SpeedBetterCompression default: encoderLevel = zstd.SpeedBestCompression } // Создаём энкодер с выбранным уровнем encoder, err = zstd.NewWriter(nil, zstd.WithEncoderLevel(encoderLevel)) if err != nil { return nil, fmt.Errorf("failed to create zstd encoder: %v", err) } defer encoder.Close() compressed = encoder.EncodeAll(data, nil) compType = CompressionZstd default: return nil, fmt.Errorf("unsupported compression algorithm: %s", config.Algorithm) } // Проверяем, что сжатие действительно уменьшило размер if len(compressed) >= len(data) { return data, nil } // Добавляем заголовок: магическое число (4 байта) + тип сжатия (1 байт) + оригинальный размер (8 байт) header := make([]byte, 4+1+8) copy(header[0:4], MagicNumber) header[4] = byte(compType) binary.LittleEndian.PutUint64(header[5:], uint64(len(data))) result := make([]byte, 0, len(header)+len(compressed)) result = append(result, header...) result = append(result, compressed...) return result, nil } // Decompress распаковывает данные func Decompress(data []byte) ([]byte, error) { // Проверяем наличие магического числа if len(data) < 4+1+8 { return nil, fmt.Errorf("data too short for compressed format") } // Проверяем магическое число if !bytes.Equal(data[0:4], MagicNumber) { return nil, fmt.Errorf("invalid magic number") } compType := CompressionType(data[4]) originalSize := binary.LittleEndian.Uint64(data[5:13]) compressedData := data[13:] if originalSize == 0 { return nil, fmt.Errorf("invalid original size") } var decompressed []byte var err error switch compType { case CompressionSnappy: decompressed, err = snappy.Decode(nil, compressedData) if err != nil { return nil, fmt.Errorf("snappy decode failed: %v", err) } case CompressionLZ4: decompressed = make([]byte, originalSize) lz4Reader := lz4.NewReader(bytes.NewReader(compressedData)) n, err := lz4Reader.Read(decompressed) if err != nil && err.Error() != "EOF" { return nil, fmt.Errorf("lz4 decode failed: %v", err) } if n != int(originalSize) { // Некоторые данные могли быть прочитаны, но не все decompressed = decompressed[:n] } case CompressionZstd: decoder, err := zstd.NewReader(nil) if err != nil { return nil, fmt.Errorf("failed to create zstd decoder: %v", err) } defer decoder.Close() decompressed, err = decoder.DecodeAll(compressedData, nil) if err != nil { return nil, fmt.Errorf("zstd decode failed: %v", err) } case CompressionNone: return compressedData, nil default: return nil, fmt.Errorf("unsupported compression type: %d", compType) } // Проверяем размер распакованных данных if len(decompressed) != int(originalSize) { // Не критично, но логируем _ = len(decompressed) } return decompressed, nil } // DecompressAuto автоматически определяет, сжаты ли данные, и распаковывает при необходимости func DecompressAuto(data []byte) ([]byte, error) { // Проверяем, есть ли магическое число (признак сжатых данных) if len(data) >= 4 && bytes.Equal(data[0:4], MagicNumber) { return Decompress(data) } return data, nil } // IsCompressed проверяет, сжаты ли данные func IsCompressed(data []byte) bool { if len(data) < 4 { return false } return bytes.Equal(data[0:4], MagicNumber) } // GetCompressionType возвращает тип сжатия данных func GetCompressionType(data []byte) CompressionType { if !IsCompressed(data) || len(data) < 5 { return CompressionNone } return CompressionType(data[4]) } // GetCompressionRatio возвращает коэффициент сжатия func GetCompressionRatio(original, compressed []byte) float64 { if len(original) == 0 { return 1.0 } return float64(len(compressed)) / float64(len(original)) }