291 lines
10 KiB
Go
291 lines
10 KiB
Go
/*
|
|||
|
|
* Copyright 2026 Safronov Grigorii
|
||
|
|
*
|
||
|
|
* Licensed under the CDDL, Version 1.0 (the "License");
|
||
|
|
* you may not use this file except in compliance with the License.
|
||
|
|
*
|
||
|
|
* You may obtain a copy of the License at
|
||
|
|
* https://opensource.org/licenses/CDDL-1.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
// Файл: internal/compression/compression.go
|
||
|
|
// Назначение: Реализация сжатия данных с использованием различных алгоритмов.
|
||
|
|
// Поддерживаемый алгоритм: Brotli.
|
||
|
|
// Обеспечивает прозрачное сжатие/распаковку для документов.
|
||
|
|
// LZ4 был удалён в пользу Brotli для лучшего версионирования.
|
||
|
|
// Защита от collision magic bytes через усиленную проверку заголовка
|
||
|
|
|
||
|
|
package compression
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/binary"
|
||
|
|
"fmt"
|
||
|
|
|
||
|
|
"github.com/golang/snappy"
|
||
|
|
"github.com/klauspost/compress/zstd"
|
||
|
|
"github.com/andybalholm/brotli"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Config представляет конфигурацию сжатия
|
||
|
|
type Config struct {
|
||
|
|
Enabled bool // Включено ли сжатие
|
||
|
|
Algorithm string // Алгоритм сжатия: snappy, brotli, zstd
|
||
|
|
Level int // Уровень сжатия (1-9)
|
||
|
|
MinSize int // Минимальный размер для сжатия (байт)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MagicNumber используется для идентификации сжатых данных
|
||
|
|
var MagicNumber = []byte{0x46, 0x54, 0x52, 0x53} // "FTRS" - Futriis
|
||
|
|
|
||
|
|
// Расширенный magic с версией для предотвращения collision
|
||
|
|
var MagicNumberV2 = []byte{0x46, 0x54, 0x52, 0x53, 0x02} // "FTRS\x02"
|
||
|
|
|
||
|
|
// MinCompressedSize — минимальный размер валидного сжатого блока
|
||
|
|
// Для защиты от collision с пользовательскими данными
|
||
|
|
const MinCompressedSize = 14 // 5 (magic v2) + 1 (type) + 8 (size)
|
||
|
|
|
||
|
|
// CompressionType определяет тип сжатия
|
||
|
|
type CompressionType byte
|
||
|
|
|
||
|
|
const (
|
||
|
|
CompressionNone CompressionType = 0x00
|
||
|
|
CompressionSnappy CompressionType = 0x01
|
||
|
|
CompressionBrotli 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 "brotli":
|
||
|
|
buf := bytes.NewBuffer(nil)
|
||
|
|
writer := brotli.NewWriter(buf)
|
||
|
|
|
||
|
|
if _, err := writer.Write(data); err != nil {
|
||
|
|
return nil, fmt.Errorf("brotli write failed: %v", err)
|
||
|
|
}
|
||
|
|
if err := writer.Close(); err != nil {
|
||
|
|
return nil, fmt.Errorf("brotli close failed: %v", err)
|
||
|
|
}
|
||
|
|
compressed = buf.Bytes()
|
||
|
|
compType = CompressionBrotli
|
||
|
|
|
||
|
|
case "zstd":
|
||
|
|
var encoder *zstd.Encoder
|
||
|
|
var encoderLevel zstd.EncoderLevel
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
// Используем MagicNumberV2 (5 байт) + type (1) + size (8) = 14 байт
|
||
|
|
header := make([]byte, 5+1+8)
|
||
|
|
copy(header[0:5], MagicNumberV2)
|
||
|
|
header[5] = byte(compType)
|
||
|
|
binary.LittleEndian.PutUint64(header[6:], uint64(len(data)))
|
||
|
|
|
||
|
|
result := make([]byte, 0, len(header)+len(compressed))
|
||
|
|
result = append(result, header...)
|
||
|
|
result = append(result, compressed...)
|
||
|
|
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decompress распаковывает данные
|
||
|
|
// Поддержка обоих форматов (V1 и V2), усиленная валидация
|
||
|
|
func Decompress(data []byte) ([]byte, error) {
|
||
|
|
// Проверяем V2 формат (5-байтовый magic)
|
||
|
|
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
||
|
|
return decompressV2(data)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Проверяем V1 формат (4-байтовый magic) для обратной совместимости
|
||
|
|
if len(data) >= 4+1+8 && bytes.Equal(data[0:4], MagicNumber) {
|
||
|
|
return decompressV1(data)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil, fmt.Errorf("invalid magic number")
|
||
|
|
}
|
||
|
|
|
||
|
|
// decompressV1 распаковывает данные в старом формате V1
|
||
|
|
func decompressV1(data []byte) ([]byte, error) {
|
||
|
|
if len(data) < 4+1+8 {
|
||
|
|
return nil, fmt.Errorf("data too short for compressed format V1")
|
||
|
|
}
|
||
|
|
|
||
|
|
compType := CompressionType(data[4])
|
||
|
|
originalSize := binary.LittleEndian.Uint64(data[5:13])
|
||
|
|
compressedData := data[13:]
|
||
|
|
|
||
|
|
if originalSize == 0 {
|
||
|
|
return nil, fmt.Errorf("invalid original size")
|
||
|
|
}
|
||
|
|
|
||
|
|
return doDecompress(compType, compressedData, originalSize)
|
||
|
|
}
|
||
|
|
|
||
|
|
// decompressV2 распаковывает данные в новом формате V2
|
||
|
|
// Добавлена проверка согласованности размера
|
||
|
|
func decompressV2(data []byte) ([]byte, error) {
|
||
|
|
if len(data) < MinCompressedSize {
|
||
|
|
return nil, fmt.Errorf("data too short for compressed format V2")
|
||
|
|
}
|
||
|
|
|
||
|
|
compType := CompressionType(data[5])
|
||
|
|
originalSize := binary.LittleEndian.Uint64(data[6:14])
|
||
|
|
compressedData := data[14:]
|
||
|
|
|
||
|
|
// Проверка разумности размера
|
||
|
|
if originalSize == 0 || originalSize > 100*1024*1024*1024 { // Max 100GB
|
||
|
|
return nil, fmt.Errorf("invalid original size: %d", originalSize)
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(compressedData) == 0 {
|
||
|
|
return nil, fmt.Errorf("no compressed data")
|
||
|
|
}
|
||
|
|
|
||
|
|
return doDecompress(compType, compressedData, originalSize)
|
||
|
|
}
|
||
|
|
|
||
|
|
// doDecompress выполняет распаковку по типу
|
||
|
|
func doDecompress(compType CompressionType, compressedData []byte, originalSize uint64) ([]byte, error) {
|
||
|
|
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 CompressionBrotli:
|
||
|
|
reader := brotli.NewReader(bytes.NewReader(compressedData))
|
||
|
|
buf := bytes.NewBuffer(nil)
|
||
|
|
_, err = buf.ReadFrom(reader)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("brotli decode failed: %v", err)
|
||
|
|
}
|
||
|
|
decompressed = buf.Bytes()
|
||
|
|
|
||
|
|
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 uint64(len(decompressed)) != originalSize {
|
||
|
|
return nil, fmt.Errorf("size mismatch: expected %d, got %d", originalSize, len(decompressed))
|
||
|
|
}
|
||
|
|
|
||
|
|
return decompressed, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// DecompressAuto автоматически определяет, сжаты ли данные, и распаковывает при необходимости
|
||
|
|
// Усиленная проверка для предотвращения collision
|
||
|
|
func DecompressAuto(data []byte) ([]byte, error) {
|
||
|
|
// Проверяем V2 magic (более надёжный)
|
||
|
|
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
||
|
|
return Decompress(data)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Проверяем V1 magic (для обратной совместимости)
|
||
|
|
if len(data) >= 4+1+8 && bytes.Equal(data[0:4], MagicNumber) {
|
||
|
|
// Дополнительная проверка: тип сжатия должен быть валидным
|
||
|
|
compType := CompressionType(data[4])
|
||
|
|
if compType <= CompressionZstd {
|
||
|
|
return Decompress(data)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Данные не сжаты
|
||
|
|
return data, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// IsCompressed проверяет, сжаты ли данные
|
||
|
|
// Проверка обоих форматов
|
||
|
|
func IsCompressed(data []byte) bool {
|
||
|
|
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if len(data) >= 4+1+8 && bytes.Equal(data[0:4], MagicNumber) {
|
||
|
|
compType := CompressionType(data[4])
|
||
|
|
return compType <= CompressionZstd
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetCompressionType возвращает тип сжатия данных
|
||
|
|
func GetCompressionType(data []byte) CompressionType {
|
||
|
|
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
||
|
|
return CompressionType(data[5])
|
||
|
|
}
|
||
|
|
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))
|
||
|
|
}
|