Update internal/compression/compression.go
This commit is contained in:
1 parent
c9a5c8307c
commit
afbc88c3e8
1 file changed
+301
-109
+301
-109
@@ -10,10 +10,44 @@
|
|||||||
|
|
||||||
// Файл: internal/compression/compression.go
|
// Файл: internal/compression/compression.go
|
||||||
// Назначение: Реализация сжатия данных с использованием различных алгоритмов.
|
// Назначение: Реализация сжатия данных с использованием различных алгоритмов.
|
||||||
// Поддерживаемый алгоритм: Brotli.
|
// Поддерживаемые алгоритмы: Snappy, Brotli, Zstd.
|
||||||
// Обеспечивает прозрачное сжатие/распаковку для документов.
|
// Обеспечивает прозрачное сжатие/распаковку для документов.
|
||||||
// LZ4 был удалён в пользу Brotli для лучшего версионирования.
|
// Защита от collision magic bytes через усиленную проверку заголовка.
|
||||||
// Защита от collision magic bytes через усиленную проверку заголовка
|
//
|
||||||
|
// ИСПРАВЛЕНО:
|
||||||
|
// 1. Compress больше не паникует при config == nil — возвращает data
|
||||||
|
// как есть (это логично: без конфигурации сжатие выключено).
|
||||||
|
// 2. Compress учитывает оверхед заголовка при решении «сжимать или нет».
|
||||||
|
// Раньше проверялось `len(compressed) >= len(data)`, но не учитывалось,
|
||||||
|
// что заголовок (13 или 14 байт) может сделать итог больше исходных
|
||||||
|
// данных. Теперь сравнение: `len(compressed) + headerSize >= len(data)`.
|
||||||
|
// 3. Level для brotli теперь применяется (brotli.NewWriterLevel), а не
|
||||||
|
// игнорируется. Диапазон уровней brotli — 0..11, поэтому входной
|
||||||
|
// Level (1..9) нормализуется.
|
||||||
|
// 4. Введены константы MagicV1HeaderSize и MagicV2HeaderSize. Раньше
|
||||||
|
// в decompressV1 стояло магическое число 13, а MinCompressedSize = 14
|
||||||
|
// относилось к V2 — легко было перепутать.
|
||||||
|
// 5. zstd-энкодер и декодер кэшируются (sync.Map). zstd.Encoder.EncodeAll
|
||||||
|
// и zstd.Decoder.DecodeAll потокобезопасны, поэтому кэш безопасен.
|
||||||
|
// Раньше энкодер/декодер создавались на каждый вызов Compress /
|
||||||
|
// doDecompress — это дорого на горячем пути.
|
||||||
|
// 6. Логика парсинга заголовка (V1/V2) вынесена в parseHeader —
|
||||||
|
// убрано дублирование между Decompress, DecompressAuto, IsCompressed,
|
||||||
|
// GetCompressionType.
|
||||||
|
// 7. В doDecompress для CompressionNone добавлена проверка размера
|
||||||
|
// (защита в глубину от malformed-входа).
|
||||||
|
// 8. bytes.Equal(data[0:N], magic) заменено на bytes.HasPrefix —
|
||||||
|
// идиоматичнее и читается лучше.
|
||||||
|
// 9. Лимит originalSize вынесен в константу MaxOriginalSize.
|
||||||
|
// 10. Обновлён комментарий про LZ4: LZ4 удалён, остались snappy/brotli/zstd.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (компиляция):
|
||||||
|
// В Compress, в ветке case "zstd", переменная err была объявлена,
|
||||||
|
// но не использовалась: zstd.Encoder.EncodeAll не возвращает ошибку.
|
||||||
|
// Убрано объявление `encoder, err := ...` — теперь `encoder, e := ...`
|
||||||
|
// с локальной переменной e, которая проверяется тут же. Внешняя err
|
||||||
|
// по-прежнему используется в других ветках (snappy/brotli) и в
|
||||||
|
// финальной проверке оверхеда заголовка.
|
||||||
|
|
||||||
package compression
|
package compression
|
||||||
|
|
||||||
@@ -21,13 +55,18 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/andybalholm/brotli"
|
||||||
"github.com/golang/snappy"
|
"github.com/golang/snappy"
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/andybalholm/brotli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config представляет конфигурацию сжатия
|
// =============================================================================
|
||||||
|
// КОНФИГУРАЦИЯ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Config представляет конфигурацию сжатия.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool // Включено ли сжатие
|
Enabled bool // Включено ли сжатие
|
||||||
Algorithm string // Алгоритм сжатия: snappy, brotli, zstd
|
Algorithm string // Алгоритм сжатия: snappy, brotli, zstd
|
||||||
@@ -35,17 +74,37 @@ type Config struct {
|
|||||||
MinSize int // Минимальный размер для сжатия (байт)
|
MinSize int // Минимальный размер для сжатия (байт)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MagicNumber используется для идентификации сжатых данных
|
// =============================================================================
|
||||||
|
// MAGIC NUMBERS И КОНСТАНТЫ ФОРМАТА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// MagicNumber используется для идентификации сжатых данных (V1).
|
||||||
var MagicNumber = []byte{0x46, 0x54, 0x52, 0x53} // "FTRS" - Futriis
|
var MagicNumber = []byte{0x46, 0x54, 0x52, 0x53} // "FTRS" - Futriis
|
||||||
|
|
||||||
// Расширенный magic с версией для предотвращения collision
|
// MagicNumberV2 — расширенный magic с версией для предотвращения collision.
|
||||||
var MagicNumberV2 = []byte{0x46, 0x54, 0x52, 0x53, 0x02} // "FTRS\x02"
|
var MagicNumberV2 = []byte{0x46, 0x54, 0x52, 0x53, 0x02} // "FTRS\x02"
|
||||||
|
|
||||||
// MinCompressedSize — минимальный размер валидного сжатого блока
|
const (
|
||||||
// Для защиты от collision с пользовательскими данными
|
// MagicV1HeaderSize — размер заголовка формата V1:
|
||||||
const MinCompressedSize = 14 // 5 (magic v2) + 1 (type) + 8 (size)
|
// 4 (magic) + 1 (type) + 8 (size) = 13 байт.
|
||||||
|
MagicV1HeaderSize = 4 + 1 + 8
|
||||||
|
|
||||||
// CompressionType определяет тип сжатия
|
// MagicV2HeaderSize — размер заголовка формата V2:
|
||||||
|
// 5 (magic) + 1 (type) + 8 (size) = 14 байт.
|
||||||
|
MagicV2HeaderSize = 5 + 1 + 8
|
||||||
|
|
||||||
|
// MinCompressedSize — минимальный размер валидного сжатого блока.
|
||||||
|
// Равен размеру заголовка V2 (самый свежий формат).
|
||||||
|
// Оставлен экспортируемым для обратной совместимости с внешним кодом,
|
||||||
|
// который мог его использовать.
|
||||||
|
MinCompressedSize = MagicV2HeaderSize
|
||||||
|
|
||||||
|
// MaxOriginalSize — верхняя граница originalSize, которую мы считаем
|
||||||
|
// разумной. 100 GiB. Защита от мусорных/враждебных заголовков.
|
||||||
|
MaxOriginalSize = uint64(100) * 1024 * 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompressionType определяет тип сжатия.
|
||||||
type CompressionType byte
|
type CompressionType byte
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -55,18 +114,143 @@ const (
|
|||||||
CompressionZstd CompressionType = 0x03
|
CompressionZstd CompressionType = 0x03
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compress сжимает данные с использованием указанного алгоритма
|
// =============================================================================
|
||||||
|
// КЭШ ZSTD ENCODER / DECODER
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// zstd.Encoder.EncodeAll и zstd.Decoder.DecodeAll потокобезопасны
|
||||||
|
// (см. документацию klauspost/compress/zstd). Поэтому один общий
|
||||||
|
// encoder/decoder можно безопасно переиспользовать из многих горутин.
|
||||||
|
//
|
||||||
|
// Кэшируем по уровню для encoder и один общий для decoder.
|
||||||
|
|
||||||
|
var (
|
||||||
|
zstdEncoders sync.Map // map[zstd.EncoderLevel]*zstd.Encoder
|
||||||
|
zstdDecoder *zstd.Decoder
|
||||||
|
zstdDecOnce sync.Once
|
||||||
|
zstdDecErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
// getZstdEncoder возвращает кэшированный zstd-энкодер для уровня.
|
||||||
|
func getZstdEncoder(level zstd.EncoderLevel) (*zstd.Encoder, error) {
|
||||||
|
if enc, ok := zstdEncoders.Load(level); ok {
|
||||||
|
return enc.(*zstd.Encoder), nil
|
||||||
|
}
|
||||||
|
enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(level))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create zstd encoder: %v", err)
|
||||||
|
}
|
||||||
|
actual, _ := zstdEncoders.LoadOrStore(level, enc)
|
||||||
|
// Если другая горутина успела первой — закрываем свой encoder.
|
||||||
|
if actual.(*zstd.Encoder) != enc {
|
||||||
|
enc.Close()
|
||||||
|
}
|
||||||
|
return actual.(*zstd.Encoder), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getZstdDecoder возвращает общий zstd-декодер (создаётся один раз).
|
||||||
|
func getZstdDecoder() (*zstd.Decoder, error) {
|
||||||
|
zstdDecOnce.Do(func() {
|
||||||
|
zstdDecoder, zstdDecErr = zstd.NewReader(nil)
|
||||||
|
})
|
||||||
|
return zstdDecoder, zstdDecErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ПАРСИНГ ЗАГОЛОВКА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// headerInfo описывает разобранный заголовок сжатого блока.
|
||||||
|
type headerInfo struct {
|
||||||
|
version int
|
||||||
|
compType CompressionType
|
||||||
|
originalSize uint64
|
||||||
|
headerSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeader пытается разобрать заголовок сжатого блока (V2 или V1).
|
||||||
|
// Возвращает nil, если данные не являются сжатым блоком.
|
||||||
|
//
|
||||||
|
// ВНИМАНИЕ: для V1 проверяется валидность compType — это защита от
|
||||||
|
// ложных срабатываний, когда несжатые данные случайно начинаются
|
||||||
|
// с байт "FTRS".
|
||||||
|
func parseHeader(data []byte) *headerInfo {
|
||||||
|
// V2: 5-байтовый magic + 1 байт type + 8 байт size.
|
||||||
|
if len(data) >= MagicV2HeaderSize && bytes.HasPrefix(data, MagicNumberV2) {
|
||||||
|
compType := CompressionType(data[5])
|
||||||
|
if isValidCompressionType(compType) {
|
||||||
|
return &headerInfo{
|
||||||
|
version: 2,
|
||||||
|
compType: compType,
|
||||||
|
originalSize: binary.LittleEndian.Uint64(data[6:14]),
|
||||||
|
headerSize: MagicV2HeaderSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Magic совпал, но тип невалиден — не считаем это сжатым блоком.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// V1: 4-байтовый magic + 1 байт type + 8 байт size.
|
||||||
|
if len(data) >= MagicV1HeaderSize && bytes.HasPrefix(data, MagicNumber) {
|
||||||
|
compType := CompressionType(data[4])
|
||||||
|
if isValidCompressionType(compType) {
|
||||||
|
return &headerInfo{
|
||||||
|
version: 1,
|
||||||
|
compType: compType,
|
||||||
|
originalSize: binary.LittleEndian.Uint64(data[5:13]),
|
||||||
|
headerSize: MagicV1HeaderSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidCompressionType проверяет, что байт соответствует известному
|
||||||
|
// типу сжатия. Используется как защита от ложных срабатываний magic.
|
||||||
|
func isValidCompressionType(t CompressionType) bool {
|
||||||
|
switch t {
|
||||||
|
case CompressionNone, CompressionSnappy, CompressionBrotli, CompressionZstd:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// СЖАТИЕ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Compress сжимает данные с использованием указанного алгоритма.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО:
|
||||||
|
// - config == nil больше не приводит к панике — возвращаем data как есть.
|
||||||
|
// - Учитывается оверхед заголовка при решении «сжимать или нет».
|
||||||
|
// - Level применяется не только к zstd, но и к brotli.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (компиляция):
|
||||||
|
// В ветке case "zstd" убрано объявление err, которое не использовалось:
|
||||||
|
// zstd.Encoder.EncodeAll не возвращает ошибку. Ошибка возможна только
|
||||||
|
// при создании encoder через getZstdEncoder — она проверяется тут же.
|
||||||
func Compress(data []byte, config *Config) ([]byte, error) {
|
func Compress(data []byte, config *Config) ([]byte, error) {
|
||||||
if !config.Enabled {
|
// config == nil трактуем как «сжатие выключено».
|
||||||
|
if config == nil || !config.Enabled {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) < config.MinSize {
|
// MinSize <= 0 означает «сжимать всё, включая пустые данные».
|
||||||
|
// Если MinSize > 0 и данные меньше — не сжимаем.
|
||||||
|
if config.MinSize > 0 && len(data) < config.MinSize {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пустые данные сжимать бессмысленно.
|
||||||
|
if len(data) == 0 {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var compressed []byte
|
var compressed []byte
|
||||||
var err error
|
|
||||||
var compType CompressionType
|
var compType CompressionType
|
||||||
|
|
||||||
switch config.Algorithm {
|
switch config.Algorithm {
|
||||||
@@ -75,10 +259,19 @@ func Compress(data []byte, config *Config) ([]byte, error) {
|
|||||||
compType = CompressionSnappy
|
compType = CompressionSnappy
|
||||||
|
|
||||||
case "brotli":
|
case "brotli":
|
||||||
|
// brotli использует уровни 0..11. Входной Level — 1..9
|
||||||
|
// (см. Config.Level). Нормализуем: <=1 -> 1, >=9 -> 11 (best).
|
||||||
|
// Это даёт предсказуемое поведение: пользователь ставит 9 —
|
||||||
|
// получает максимальное сжатие.
|
||||||
|
level := normalizeBrotliLevel(config.Level)
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
writer := brotli.NewWriter(buf)
|
writer := brotli.NewWriterLevel(buf, level)
|
||||||
|
|
||||||
if _, err := writer.Write(data); err != nil {
|
if _, err := writer.Write(data); err != nil {
|
||||||
|
// writer.Close() стоит вызвать даже при ошибке Write,
|
||||||
|
// чтобы освободить ресурсы, но ошибку Close игнорируем —
|
||||||
|
// основная уже есть.
|
||||||
|
_ = writer.Close()
|
||||||
return nil, fmt.Errorf("brotli write failed: %v", err)
|
return nil, fmt.Errorf("brotli write failed: %v", err)
|
||||||
}
|
}
|
||||||
if err := writer.Close(); err != nil {
|
if err := writer.Close(); err != nil {
|
||||||
@@ -88,9 +281,7 @@ func Compress(data []byte, config *Config) ([]byte, error) {
|
|||||||
compType = CompressionBrotli
|
compType = CompressionBrotli
|
||||||
|
|
||||||
case "zstd":
|
case "zstd":
|
||||||
var encoder *zstd.Encoder
|
|
||||||
var encoderLevel zstd.EncoderLevel
|
var encoderLevel zstd.EncoderLevel
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case config.Level <= 1:
|
case config.Level <= 1:
|
||||||
encoderLevel = zstd.SpeedFastest
|
encoderLevel = zstd.SpeedFastest
|
||||||
@@ -102,12 +293,15 @@ func Compress(data []byte, config *Config) ([]byte, error) {
|
|||||||
encoderLevel = zstd.SpeedBestCompression
|
encoderLevel = zstd.SpeedBestCompression
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder, err = zstd.NewWriter(nil, zstd.WithEncoderLevel(encoderLevel))
|
// ИСПРАВЛЕНО: вместо `encoder, err := ...` используем локальную e,
|
||||||
if err != nil {
|
// чтобы не объявлять неиспользуемую err. getZstdEncoder возвращает
|
||||||
return nil, fmt.Errorf("failed to create zstd encoder: %v", err)
|
// (*zstd.Encoder, error), и эта ошибка проверяется тут же.
|
||||||
|
encoder, e := getZstdEncoder(encoderLevel)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
}
|
}
|
||||||
defer encoder.Close()
|
|
||||||
|
|
||||||
|
// EncodeAll потокобезопасен, используем общий encoder.
|
||||||
compressed = encoder.EncodeAll(data, nil)
|
compressed = encoder.EncodeAll(data, nil)
|
||||||
compType = CompressionZstd
|
compType = CompressionZstd
|
||||||
|
|
||||||
@@ -115,16 +309,18 @@ func Compress(data []byte, config *Config) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("unsupported compression algorithm: %s", config.Algorithm)
|
return nil, fmt.Errorf("unsupported compression algorithm: %s", config.Algorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что сжатие действительно уменьшило размер
|
// ИСПРАВЛЕНО: учитываем оверхед заголовка V2.
|
||||||
if len(compressed) >= len(data) {
|
// Если сжатые данные + заголовок не меньше исходных —
|
||||||
|
// сжатие не имеет смысла, возвращаем исходные данные.
|
||||||
|
if len(compressed)+MagicV2HeaderSize >= len(data) {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Используем MagicNumberV2 (5 байт) + type (1) + size (8) = 14 байт
|
// Формируем заголовок V2: 5 (magic) + 1 (type) + 8 (size).
|
||||||
header := make([]byte, 5+1+8)
|
header := make([]byte, MagicV2HeaderSize)
|
||||||
copy(header[0:5], MagicNumberV2)
|
copy(header[0:5], MagicNumberV2)
|
||||||
header[5] = byte(compType)
|
header[5] = byte(compType)
|
||||||
binary.LittleEndian.PutUint64(header[6:], uint64(len(data)))
|
binary.LittleEndian.PutUint64(header[6:14], uint64(len(data)))
|
||||||
|
|
||||||
result := make([]byte, 0, len(header)+len(compressed))
|
result := make([]byte, 0, len(header)+len(compressed))
|
||||||
result = append(result, header...)
|
result = append(result, header...)
|
||||||
@@ -133,63 +329,61 @@ func Compress(data []byte, config *Config) ([]byte, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decompress распаковывает данные
|
// normalizeBrotliLevel приводит входной уровень (1..9) к диапазону
|
||||||
// Поддержка обоих форматов (V1 и V2), усиленная валидация
|
// brotli (0..11). Значения <=0 и >9 трактуются разумно:
|
||||||
|
// - level <= 0 -> brotli.DefaultCompression
|
||||||
|
// - level 1..9 -> линейное отображение: 1->1, 9->11
|
||||||
|
// - level > 9 -> 11 (максимум)
|
||||||
|
func normalizeBrotliLevel(level int) int {
|
||||||
|
if level <= 0 {
|
||||||
|
return brotli.DefaultCompression
|
||||||
|
}
|
||||||
|
if level >= 9 {
|
||||||
|
return 11
|
||||||
|
}
|
||||||
|
// Линейное отображение 1..9 -> 1..11.
|
||||||
|
// 1->1, 2->2, ..., 9->11. Формула: 1 + (level-1)*10/8.
|
||||||
|
return 1 + (level-1)*10/8
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// РАСПАКОВКА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Decompress распаковывает данные.
|
||||||
|
// Поддерживает оба формата (V1 и V2), усиленная валидация.
|
||||||
func Decompress(data []byte) ([]byte, error) {
|
func Decompress(data []byte) ([]byte, error) {
|
||||||
// Проверяем V2 формат (5-байтовый magic)
|
info := parseHeader(data)
|
||||||
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
if info == nil {
|
||||||
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")
|
return nil, fmt.Errorf("invalid magic number")
|
||||||
|
}
|
||||||
|
return decompressWithInfo(data, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// decompressV1 распаковывает данные в старом формате V1
|
// decompressWithInfo выполняет распаковку, используя разобранный заголовок.
|
||||||
func decompressV1(data []byte) ([]byte, error) {
|
func decompressWithInfo(data []byte, info *headerInfo) ([]byte, error) {
|
||||||
if len(data) < 4+1+8 {
|
if len(data) < info.headerSize {
|
||||||
return nil, fmt.Errorf("data too short for compressed format V1")
|
return nil, fmt.Errorf("data too short for compressed format V%d", info.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
compType := CompressionType(data[4])
|
// Проверка разумности размера.
|
||||||
originalSize := binary.LittleEndian.Uint64(data[5:13])
|
if info.originalSize == 0 || info.originalSize > MaxOriginalSize {
|
||||||
compressedData := data[13:]
|
return nil, fmt.Errorf("invalid original size: %d", info.originalSize)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compressedData := data[info.headerSize:]
|
||||||
if len(compressedData) == 0 {
|
if len(compressedData) == 0 {
|
||||||
return nil, fmt.Errorf("no compressed data")
|
return nil, fmt.Errorf("no compressed data")
|
||||||
}
|
}
|
||||||
|
|
||||||
return doDecompress(compType, compressedData, originalSize)
|
return doDecompress(info.compType, compressedData, info.originalSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// doDecompress выполняет распаковку по типу
|
// doDecompress выполняет распаковку по типу.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО:
|
||||||
|
// - для CompressionNone добавлена проверка размера (защита в глубину);
|
||||||
|
// - zstd-декодер берётся из кэша, а не создаётся на каждый вызов.
|
||||||
func doDecompress(compType CompressionType, compressedData []byte, originalSize uint64) ([]byte, error) {
|
func doDecompress(compType CompressionType, compressedData []byte, originalSize uint64) ([]byte, error) {
|
||||||
var decompressed []byte
|
var decompressed []byte
|
||||||
var err error
|
var err error
|
||||||
@@ -211,25 +405,32 @@ func doDecompress(compType CompressionType, compressedData []byte, originalSize
|
|||||||
decompressed = buf.Bytes()
|
decompressed = buf.Bytes()
|
||||||
|
|
||||||
case CompressionZstd:
|
case CompressionZstd:
|
||||||
decoder, err := zstd.NewReader(nil)
|
decoder, err := getZstdDecoder()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create zstd decoder: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer decoder.Close()
|
// DecodeAll потокобезопасен, используем общий decoder.
|
||||||
|
|
||||||
decompressed, err = decoder.DecodeAll(compressedData, nil)
|
decompressed, err = decoder.DecodeAll(compressedData, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("zstd decode failed: %v", err)
|
return nil, fmt.Errorf("zstd decode failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case CompressionNone:
|
case CompressionNone:
|
||||||
|
// ИСПРАВЛЕНО: проверяем согласованность размера даже для None.
|
||||||
|
// Это защита в глубину от malformed-входа. Сейчас Compress не
|
||||||
|
// пишет CompressionNone в заголовок, но на входе может оказаться
|
||||||
|
// подделанный блок.
|
||||||
|
if uint64(len(compressedData)) != originalSize {
|
||||||
|
return nil, fmt.Errorf("size mismatch for uncompressed: expected %d, got %d",
|
||||||
|
originalSize, len(compressedData))
|
||||||
|
}
|
||||||
return compressedData, nil
|
return compressedData, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported compression type: %d", compType)
|
return nil, fmt.Errorf("unsupported compression type: %d", compType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что распакованный размер совпадает с заявленным
|
// Проверяем, что распакованный размер совпадает с заявленным.
|
||||||
if uint64(len(decompressed)) != originalSize {
|
if uint64(len(decompressed)) != originalSize {
|
||||||
return nil, fmt.Errorf("size mismatch: expected %d, got %d", originalSize, len(decompressed))
|
return nil, fmt.Errorf("size mismatch: expected %d, got %d", originalSize, len(decompressed))
|
||||||
}
|
}
|
||||||
@@ -237,52 +438,43 @@ func doDecompress(compType CompressionType, compressedData []byte, originalSize
|
|||||||
return decompressed, nil
|
return decompressed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecompressAuto автоматически определяет, сжаты ли данные, и распаковывает при необходимости
|
// DecompressAuto автоматически определяет, сжаты ли данные, и распаковывает при необходимости.
|
||||||
// Усиленная проверка для предотвращения collision
|
// Усиленная проверка для предотвращения collision.
|
||||||
func DecompressAuto(data []byte) ([]byte, error) {
|
func DecompressAuto(data []byte) ([]byte, error) {
|
||||||
// Проверяем V2 magic (более надёжный)
|
info := parseHeader(data)
|
||||||
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
if info == nil {
|
||||||
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
|
return data, nil
|
||||||
|
}
|
||||||
|
return decompressWithInfo(data, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsCompressed проверяет, сжаты ли данные
|
// IsCompressed проверяет, сжаты ли данные (V1 или V2).
|
||||||
// Проверка обоих форматов
|
|
||||||
func IsCompressed(data []byte) bool {
|
func IsCompressed(data []byte) bool {
|
||||||
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
return parseHeader(data) != nil
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(data) >= 4+1+8 && bytes.Equal(data[0:4], MagicNumber) {
|
|
||||||
compType := CompressionType(data[4])
|
|
||||||
return compType <= CompressionZstd
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompressionType возвращает тип сжатия данных
|
// GetCompressionType возвращает тип сжатия данных.
|
||||||
|
// Возвращает CompressionNone, если данные не сжаты.
|
||||||
func GetCompressionType(data []byte) CompressionType {
|
func GetCompressionType(data []byte) CompressionType {
|
||||||
if len(data) >= MinCompressedSize && bytes.Equal(data[0:5], MagicNumberV2) {
|
info := parseHeader(data)
|
||||||
return CompressionType(data[5])
|
if info == nil {
|
||||||
}
|
|
||||||
if !IsCompressed(data) || len(data) < 5 {
|
|
||||||
return CompressionNone
|
return CompressionNone
|
||||||
}
|
}
|
||||||
return CompressionType(data[4])
|
return info.compType
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompressionRatio возвращает коэффициент сжатия
|
// =============================================================================
|
||||||
|
// СТАТИСТИКА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// GetCompressionRatio возвращает коэффициент сжатия как долю:
|
||||||
|
// - 1.0 — данные не уменьшились (или выросли);
|
||||||
|
// - 0.5 — сжатые данные в два раза меньше исходных;
|
||||||
|
// - 0.0 — теоретический предел (пустые данные после сжатия).
|
||||||
|
//
|
||||||
|
// ВНИМАНИЕ: это именно доля compressed/original, а не «во сколько раз
|
||||||
|
// сжали». Если нужно «во сколько раз», используйте 1.0 / ratio.
|
||||||
func GetCompressionRatio(original, compressed []byte) float64 {
|
func GetCompressionRatio(original, compressed []byte) float64 {
|
||||||
if len(original) == 0 {
|
if len(original) == 0 {
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|||||||
Reference in new issue
Block a user