mirror of
https://github.com/luxfi/compress.git
synced 2026-10-30 16:59:35 +00:00
github.com/luxfi/utils is now github.com/luxfi/util. Import paths and the module requirement follow the rename, and tidying pulled the dependency graph that v1.3.4 carries. Co-authored-by: Hanzo Dev <[email protected]>
246 lines
6.2 KiB
Go
246 lines
6.2 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package compress
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
_ "embed"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/util"
|
|
)
|
|
|
|
const maxMessageSize = 2 * constants.MiB // Max message size. Can't import due to cycle.
|
|
|
|
var (
|
|
newCompressorFuncs = map[Type]func(maxSize int64) (Compressor, error){
|
|
TypeNone: func(int64) (Compressor, error) { //nolint:unparam // an error is needed to be returned to compile
|
|
return NewNoCompressor(), nil
|
|
},
|
|
TypeZstd: NewZstdCompressor,
|
|
}
|
|
|
|
//go:embed zstd_zip_bomb.bin
|
|
zstdZipBomb []byte
|
|
|
|
zipBombs = map[Type][]byte{
|
|
TypeZstd: zstdZipBomb,
|
|
}
|
|
)
|
|
|
|
func TestDecompressZipBombs(t *testing.T) {
|
|
for compressionType, zipBomb := range zipBombs {
|
|
// Make sure that the hardcoded zip bomb would be a valid message.
|
|
require.Less(t, len(zipBomb), maxMessageSize)
|
|
|
|
newCompressorFunc := newCompressorFuncs[compressionType]
|
|
|
|
t.Run(compressionType.String(), func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
compressor, err := newCompressorFunc(maxMessageSize)
|
|
require.NoError(err)
|
|
|
|
var (
|
|
beforeDecompressionStats runtime.MemStats
|
|
afterDecompressionStats runtime.MemStats
|
|
)
|
|
runtime.ReadMemStats(&beforeDecompressionStats)
|
|
_, err = compressor.Decompress(zipBomb)
|
|
runtime.ReadMemStats(&afterDecompressionStats)
|
|
|
|
require.ErrorIs(err, ErrDecompressedMsgTooLarge)
|
|
|
|
// Make sure that we didn't allocate significantly more memory than
|
|
// the max message size.
|
|
bytesAllocatedDuringDecompression := afterDecompressionStats.TotalAlloc - beforeDecompressionStats.TotalAlloc
|
|
require.Less(bytesAllocatedDuringDecompression, uint64(10*maxMessageSize))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompressDecompress(t *testing.T) {
|
|
for compressionType, newCompressorFunc := range newCompressorFuncs {
|
|
t.Run(compressionType.String(), func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
data := crypto.RandomBytes(4096)
|
|
data2 := crypto.RandomBytes(4096)
|
|
|
|
compressor, err := newCompressorFunc(maxMessageSize)
|
|
require.NoError(err)
|
|
|
|
dataCompressed, err := compressor.Compress(data)
|
|
require.NoError(err)
|
|
|
|
data2Compressed, err := compressor.Compress(data2)
|
|
require.NoError(err)
|
|
|
|
dataDecompressed, err := compressor.Decompress(dataCompressed)
|
|
require.NoError(err)
|
|
require.Equal(data, dataDecompressed)
|
|
|
|
data2Decompressed, err := compressor.Decompress(data2Compressed)
|
|
require.NoError(err)
|
|
require.Equal(data2, data2Decompressed)
|
|
|
|
dataDecompressed, err = compressor.Decompress(dataCompressed)
|
|
require.NoError(err)
|
|
require.Equal(data, dataDecompressed)
|
|
|
|
maxMessage := crypto.RandomBytes(maxMessageSize)
|
|
maxMessageCompressed, err := compressor.Compress(maxMessage)
|
|
require.NoError(err)
|
|
|
|
maxMessageDecompressed, err := compressor.Decompress(maxMessageCompressed)
|
|
require.NoError(err)
|
|
|
|
require.Equal(maxMessage, maxMessageDecompressed)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSizeLimiting(t *testing.T) {
|
|
for compressionType, compressorFunc := range newCompressorFuncs {
|
|
if compressionType == TypeNone {
|
|
continue
|
|
}
|
|
t.Run(compressionType.String(), func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
compressor, err := compressorFunc(maxMessageSize)
|
|
require.NoError(err)
|
|
|
|
data := make([]byte, maxMessageSize+1)
|
|
_, err = compressor.Compress(data) // should be too large
|
|
require.ErrorIs(err, ErrMsgTooLarge)
|
|
|
|
compressor2, err := compressorFunc(2 * maxMessageSize)
|
|
require.NoError(err)
|
|
|
|
dataCompressed, err := compressor2.Compress(data)
|
|
require.NoError(err)
|
|
|
|
_, err = compressor.Decompress(dataCompressed) // should be too large
|
|
require.ErrorIs(err, ErrDecompressedMsgTooLarge)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Attempts to create a compressor with math.MaxInt64
|
|
// which leads to undefined decompress behavior due to integer overflow
|
|
// in limit reader creation.
|
|
func TestNewCompressorWithInvalidLimit(t *testing.T) {
|
|
for compressionType, compressorFunc := range newCompressorFuncs {
|
|
if compressionType == TypeNone {
|
|
continue
|
|
}
|
|
t.Run(compressionType.String(), func(t *testing.T) {
|
|
_, err := compressorFunc(math.MaxInt64)
|
|
require.ErrorIs(t, err, ErrInvalidMaxSizeCompressor)
|
|
})
|
|
}
|
|
}
|
|
|
|
func FuzzZstdCompressor(f *testing.F) {
|
|
fuzzHelper(f, TypeZstd)
|
|
}
|
|
|
|
func fuzzHelper(f *testing.F, compressionType Type) {
|
|
var (
|
|
compressor Compressor
|
|
err error
|
|
)
|
|
switch compressionType {
|
|
case TypeZstd:
|
|
compressor, err = NewZstdCompressor(maxMessageSize)
|
|
require.NoError(f, err)
|
|
default:
|
|
require.FailNow(f, "Unknown compression type")
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
require := require.New(t)
|
|
|
|
if len(data) > maxMessageSize {
|
|
_, err := compressor.Compress(data)
|
|
require.ErrorIs(err, ErrMsgTooLarge)
|
|
}
|
|
|
|
compressed, err := compressor.Compress(data)
|
|
require.NoError(err)
|
|
|
|
decompressed, err := compressor.Decompress(compressed)
|
|
require.NoError(err)
|
|
|
|
require.Equal(data, decompressed)
|
|
})
|
|
}
|
|
|
|
func BenchmarkCompress(b *testing.B) {
|
|
sizes := []int{
|
|
0,
|
|
256,
|
|
constants.KiB,
|
|
constants.MiB,
|
|
maxMessageSize,
|
|
}
|
|
for compressionType, newCompressorFunc := range newCompressorFuncs {
|
|
if compressionType == TypeNone {
|
|
continue
|
|
}
|
|
for _, size := range sizes {
|
|
b.Run(fmt.Sprintf("%s_%d", compressionType, size), func(b *testing.B) {
|
|
require := require.New(b)
|
|
|
|
bytes := crypto.RandomBytes(size)
|
|
compressor, err := newCompressorFunc(maxMessageSize)
|
|
require.NoError(err)
|
|
for n := 0; n < b.N; n++ {
|
|
_, err := compressor.Compress(bytes)
|
|
require.NoError(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkDecompress(b *testing.B) {
|
|
sizes := []int{
|
|
0,
|
|
256,
|
|
constants.KiB,
|
|
constants.MiB,
|
|
maxMessageSize,
|
|
}
|
|
for compressionType, newCompressorFunc := range newCompressorFuncs {
|
|
if compressionType == TypeNone {
|
|
continue
|
|
}
|
|
for _, size := range sizes {
|
|
b.Run(fmt.Sprintf("%s_%d", compressionType, size), func(b *testing.B) {
|
|
require := require.New(b)
|
|
|
|
bytes := crypto.RandomBytes(size)
|
|
compressor, err := newCompressorFunc(maxMessageSize)
|
|
require.NoError(err)
|
|
|
|
compressedBytes, err := compressor.Compress(bytes)
|
|
require.NoError(err)
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
_, err := compressor.Decompress(compressedBytes)
|
|
require.NoError(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|