Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ Teeworlds huffman compression library.
## Installation

```shell
// for latest tagged release
# for latest tagged release
go get github.com/teeworlds-go/huffman/v2@latest

// for bleeding edge master branch version
# for bleeding edge master branch version
go get github.com/teeworlds-go/huffman@master
```

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/teeworlds-go/huffman/v2

go 1.22.3
go 1.24.0
14 changes: 13 additions & 1 deletion huffman.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,20 @@ func (huff *Huffman) Decompress(data []byte) ([]byte, error) {
if len(data) == 0 {
return []byte{}, nil
}
return huff.DecompressTo(nil, data)
}

// DecompressTo decompresses data, APPENDING the decompressed bytes to dst and
// returning the extended slice (like Go's append). Pass a reused buffer's
// dst[:0] to avoid allocating a fresh output slice on every call — useful on
// hot paths that decompress many packets (e.g. a 50Hz snapshot stream). dst may
// be nil. huff is not modified, so a single Huffman value is safe for
// concurrent DecompressTo calls with distinct dst buffers.
func (huff *Huffman) DecompressTo(dst, data []byte) ([]byte, error) {
if len(data) == 0 {
return dst, nil
}

dst := []byte{}
srcIndex := 0
size := len(data)
bits := uint32(0)
Expand Down
56 changes: 56 additions & 0 deletions huffman_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,59 @@ func FuzzHuffmannCompressDecompress(f *testing.F) {
}
})
}

// DecompressTo must produce byte-identical output to Decompress, regardless of
// what the caller-supplied buffer already holds (its old contents are discarded).
func TestDecompressToMatchesDecompress(t *testing.T) {
huff := NewHuffman()

inputs := [][]byte{
[]byte("hello world"),
[]byte("1234567890abcdef1234567890abcdef"),
bytes.Repeat([]byte("teeworlds snapshot payload "), 32),
}

// A reused buffer with stale contents, to prove DecompressTo resets it.
reuse := make([]byte, 0, 512)
reuse = append(reuse, "stale"...)

for _, in := range inputs {
compressed, err := huff.Compress(in)
if err != nil {
t.Fatalf("compress: %v", err)
}

want, err := huff.Decompress(compressed)
if err != nil {
t.Fatalf("decompress: %v", err)
}

got, err := huff.DecompressTo(reuse[:0], compressed)
if err != nil {
t.Fatalf("decompress to: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("DecompressTo mismatch: got %v want %v", got, want)
}
reuse = got // keep growing the shared buffer across iterations
}
}

// BenchmarkDecompressTo shows that reusing the output buffer across calls
// reaches a steady state of 0 allocs/op (the whole reason DecompressTo exists).
func BenchmarkDecompressTo(b *testing.B) {
huff := NewHuffman()
compressed, err := huff.Compress(bytes.Repeat([]byte("teeworlds snapshot payload "), 32))
if err != nil {
b.Fatal(err)
}

buf := make([]byte, 0, 4096)
b.ReportAllocs()
for b.Loop() {
buf, err = huff.DecompressTo(buf[:0], compressed)
if err != nil {
b.Fatal(err)
}
}
}
Loading