diff --git a/v2/autoclose.go b/v2/autoclose.go new file mode 100644 index 0000000..5c412fa --- /dev/null +++ b/v2/autoclose.go @@ -0,0 +1,16 @@ +package libdeflate + +import "runtime" + +// panicFreeCloser is a type that can be closed without panicking if it is already closed. This is used for AutoClose functionality. +type panicFreeCloser interface { + PanicFreeClose() +} + +// attachAutoClose attaches a finalizer to the given panicFreeCloser that calls PanicFreeClose() when the object is garbage collected. +// Used for AutoClose functionality. Do not call this function directly. +func attachAutoClose(c panicFreeCloser) { + runtime.SetFinalizer(c, func(finalized panicFreeCloser) { + finalized.PanicFreeClose() + }) +} diff --git a/v2/compressor.go b/v2/compressor.go index 0029fcf..2093717 100644 --- a/v2/compressor.go +++ b/v2/compressor.go @@ -1,19 +1,43 @@ package libdeflate -import "github.com/4kills/go-libdeflate/v2/native" +import ( + "github.com/4kills/go-libdeflate/v2/native" +) // Compressor compresses data at the specified compression level. // // A single compressor must not not be used across multiple threads concurrently. // If you want to compress concurrently, create a compressor for each thread. // -// Always Close() the decompressor to free c memory. +// Always Close() the decompressor to free c memory or use the AutoClose constructors, which will automatically close the Compressor at garabage collection time of the underlying natvie equivalent. // One Compressor allocates at least 32 KiB. type Compressor struct { c *native.Compressor lvl int } +// NewCompressorAutoClose returns a Compressor used to compress data with compression level DefaultCompressionLevel and AutoClose functionality. +// This will close the Compressor automatically, when it is no longer used. +// Errors if out of memory. Allocates 32KiB. +// See NewCompressorLevelPointer for custom compression level +func NewCompressorAutoClose() (Compressor, error) { + compressor, err := NewCompressorLevelAutoClose(DefaultCompressionLevel) + return compressor, err +} + +// NewCompressorLevelAutoClose returns a new pointer to a Compressor used to compress data and AutoClose functionality. +// This will close the Compressor automatically, when it is no longer used. +// Errors if out of memory or if an invalid compression level was passed. +// Allocates 32KiB. +// +// The compression level is legal if and only if: +// MinCompressionLevel <= level <= MaxCompressionLevel +func NewCompressorLevelAutoClose(level int) (Compressor, error) { + compressor, err := NewCompressorLevel(level) + attachAutoClose(compressor.c) + return compressor, err +} + // NewCompressor returns a new Compressor used to compress data with compression level DefaultCompressionLevel. // Errors if out of memory. Allocates 32KiB. // See NewCompressorLevel for custom compression level diff --git a/v2/compressor_test.go b/v2/compressor_test.go index d7e9f1f..cf33205 100644 --- a/v2/compressor_test.go +++ b/v2/compressor_test.go @@ -101,6 +101,22 @@ func TestCompressZlib(t *testing.T) { slicesEqual([]byte(shortString), decomp, t) } +func TestCompressZlib_AutoClose(t *testing.T) { + c, _ := NewCompressorAutoClose() + defer c.Close() + _, comp, err := c.CompressZlib(shortString, nil) + if err != nil { + t.Error(err) + } + + r, _ := zlib.NewReader(bytes.NewBuffer(comp)) + defer r.Close() + decomp := make([]byte, len(shortString)) + r.Read(decomp) + + slicesEqual([]byte(shortString), decomp, t) +} + // this test doesn't really say as much as TestCompress func TestCompressMeta(t *testing.T) { c, _ := NewCompressor() @@ -141,7 +157,7 @@ func TestCompressDecompress(t *testing.T) { out := make([]byte, len(shortString)) dc, _ := NewDecompressor() defer dc.Close() - if c, _, err := dc.DecompressZlib(comp, out); err != nil || c != len(comp){ + if c, _, err := dc.DecompressZlib(comp, out); err != nil || c != len(comp) { t.Error(err) } slicesEqual(shortString, out, t) diff --git a/v2/decompressor.go b/v2/decompressor.go index f2079ef..6682acb 100644 --- a/v2/decompressor.go +++ b/v2/decompressor.go @@ -7,12 +7,32 @@ import "github.com/4kills/go-libdeflate/v2/native" // A single decompressor must not not be used across multiple threads concurrently. // If you want to decompress concurrently, create a decompressor for each thread. // -// Always Close() the decompressor to free c memory. +// Always Close() the decompressor to free c memory or use the AutoClose constructors, which will automatically close the Compressor at garabage collection time of the underlying natvie equivalent. // One Decompressor allocates at least 32KiB. type Decompressor struct { dc *native.Decompressor } +// NewDecompressor returns a new Decompressor used to decompress data at any compression level and with any Mode. It has AutoClose functionality. +// This will close the Decompressor automatically, when it is no longer used. +// Errors if out of memory. Allocates 32KiB. +func NewDecompressorAutoClose() (Decompressor, error) { + decompressor, err := NewDecompressor() + attachAutoClose(decompressor.dc) + return decompressor, err +} + +// NewDecompressorWithExtendedDecompression returns a new Decompressor used to decompress data at any compression level and with any Mode. +// It has AutoClose functionality, which will close the Decompressor automatically, when it is no longer used. +// maxDecompressionFactor customizes how much larger your output than your input may be. This is set to a sensible default, +// however, it might need some tweaking if you have a huge compression factor. Usually, NewDecompressor should suffice. +// Errors if out of memory. Allocates 32KiB. +func NewDecompressorAutoCloseWithExtendedDecompression(maxDecompressionFactor int) (Decompressor, error) { + decompressor, err := NewDecompressorWithExtendedDecompression(maxDecompressionFactor) + attachAutoClose(decompressor.dc) + return decompressor, err +} + // NewDecompressor returns a new Decompressor used to decompress data at any compression level and with any Mode. // Errors if out of memory. Allocates 32KiB. func NewDecompressor() (Decompressor, error) { diff --git a/v2/decompressor_test.go b/v2/decompressor_test.go index bb03b1d..e3fc19b 100644 --- a/v2/decompressor_test.go +++ b/v2/decompressor_test.go @@ -89,6 +89,31 @@ func TestDecompressZlib(t *testing.T) { slicesEqual(shortString, out, t) } +func TestDecompressZlib_AutoClose(t *testing.T) { + // compress with go standard lib + buf := &bytes.Buffer{} + w := zlib.NewWriter(buf) + w.Write(shortString) + w.Close() + in := buf.Bytes() + + // decompress with this lib + + out := make([]byte, len(shortString)) + dc, _ := NewDecompressorAutoClose() + defer dc.Close() + if c, _, err := dc.DecompressZlib(in, out); err != nil || c != len(in) { + t.Error(err) + } + slicesEqual(shortString, out, t) + + c, out, err := dc.DecompressZlib(in, nil) + if err != nil || c != len(in) { + t.Error(err) + } + slicesEqual(shortString, out, t) +} + var ( deadlyCode = "789c7d90316f83301085f7fc0ac45cac3b1b63cc0685542c55a4264b1784825bd1024686284851fe7b8104c454c" + "b8bdff7de9def6e3b6b3cf6db298dedc0b26f02384014858efb2af78ecb1573c224e24e92301e4919d158c0dd7e7" + diff --git a/v2/native/compressor.go b/v2/native/compressor.go index 08c2cf3..572e9a9 100644 --- a/v2/native/compressor.go +++ b/v2/native/compressor.go @@ -91,3 +91,11 @@ func (c *Compressor) Close() { C.libdeflate_free_compressor(c.c) c.isClosed = true } + +// PanicFreeClose is like Close but doesn't panic if the compressor is already closed. This is useful for the higher-level autoclose functionality. +func (c *Compressor) PanicFreeClose() { + if c.isClosed { + return + } + c.Close() +} diff --git a/v2/native/decompressor.go b/v2/native/decompressor.go index bde6f4c..7577491 100644 --- a/v2/native/decompressor.go +++ b/v2/native/decompressor.go @@ -14,8 +14,8 @@ import "unsafe" // Decompressor decompresses any DEFLATE, zlib or gzip compressed data at any level type Decompressor struct { - dc *C.decomp - isClosed bool + dc *C.decomp + isClosed bool maxDecompressionFactor int } @@ -80,7 +80,7 @@ func (dc *Decompressor) decompress(in, out []byte, fit bool, f decompress) (int, var ( cons int - n int + n int ) consPtr := uintptr(unsafe.Pointer(&cons)) @@ -106,3 +106,11 @@ func (dc *Decompressor) Close() { C.libdeflate_free_decompressor(dc.dc) dc.isClosed = true } + +// PanicFreeClose is like Close but doesn't panic if the decompressor is already closed. This is useful for the higher-level autoclose functionality. +func (c *Decompressor) PanicFreeClose() { + if c.isClosed { + return + } + c.Close() +}