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
8 changes: 8 additions & 0 deletions dictionary.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ func NewDictionary() *Dictionary {
return NewDictionaryWithFrequencies(TeeworldsFrequencyTable)
}

// isInitialized reports whether d was built by one of the dictionary
// constructors. Dictionary has exported type so callers can create its zero
// value, but its tables are intentionally private and a zero-value dictionary
// cannot encode or decode a valid Huffman stream.
func (d *Dictionary) isInitialized() bool {
return d != nil && d.numNodes == maxNodes && d.maxCodeLen != 0
}

func NewDictionaryWithFrequencies(frequencyTable [MaxSymbols]uint32) *Dictionary {

d := Dictionary{}
Expand Down
39 changes: 37 additions & 2 deletions huffman.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package huffman
import (
"encoding/binary"
"fmt"
"unsafe"
)

const (
Expand Down Expand Up @@ -71,6 +72,9 @@ func NewHuffmanDict(d *Dictionary) *Huffman {
// consumes more bits than the input actually contains, so a stream that does
// not carry an EOF symbol returns an error instead of looping forever.
func (huff *Huffman) Decompress(data []byte) ([]byte, error) {
if huff == nil || !huff.Dictionary.isInitialized() {
return nil, fmt.Errorf("%w: dictionary is nil or uninitialized", ErrHuffmanDecompress)
}
if len(data) == 0 {
return []byte{}, nil
}
Expand All @@ -81,9 +85,13 @@ func (huff *Huffman) Decompress(data []byte) ([]byte, error) {
// 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.
// be nil and may overlap data; overlapping source bytes are preserved before
// output is appended. 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 huff == nil || !huff.Dictionary.isInitialized() {
return nil, fmt.Errorf("%w: dictionary is nil or uninitialized", ErrHuffmanDecompress)
}
if len(data) == 0 {
return dst, nil
}
Expand All @@ -95,6 +103,14 @@ func (huff *Huffman) DecompressTo(dst, data []byte) ([]byte, error) {
lut := &d.decLut
nodes := &d.nodes

// Decompression can expand one input byte into several output bytes. If
// dst's writable capacity overlaps data, appending output could therefore
// overwrite compressed bytes before the decoder consumes them. Preserve
// append-style in-place use by copying only in this exceptional case.
if byteSlicesOverlap(dst[len(dst):cap(dst)], data) {
data = append([]byte(nil), data...)
}

// Output sizing. Two competing costs: guessing low means realloc+copy,
// guessing high wastes memory and page faults.
//
Expand Down Expand Up @@ -269,6 +285,9 @@ bulk:
// require. Returning an empty slice here would produce a stream neither of
// them can decode.
func (huff *Huffman) Compress(data []byte) ([]byte, error) {
if huff == nil || !huff.Dictionary.isInitialized() {
return nil, fmt.Errorf("%w: dictionary is nil or uninitialized", ErrHuffmanCompress)
}
d := huff.Dictionary

// Codes are stored as uint32 in Dictionary. Reject deeper custom trees
Expand Down Expand Up @@ -339,6 +358,22 @@ func (huff *Huffman) Compress(data []byte) ([]byte, error) {
return dst[:pos], nil
}

// byteSlicesOverlap reports whether the two slice ranges share any byte. It
// compares addresses only; it never converts uintptr values back to pointers.
// Using subtraction instead of computing end addresses also avoids uintptr
// overflow on 32 bit platforms.
func byteSlicesOverlap(a, b []byte) bool {
if len(a) == 0 || len(b) == 0 {
return false
}
aStart := uintptr(unsafe.Pointer(unsafe.SliceData(a)))
bStart := uintptr(unsafe.Pointer(unsafe.SliceData(b)))
if aStart <= bStart {
return bStart-aStart < uintptr(len(a))
}
return aStart-bStart < uintptr(len(b))
}

// Buffer sizing arithmetic, kept in one place and parameterised by limit (the
// platform's maxAlloc) so the 32 bit behaviour is unit-testable on any host.
// All of it runs in uint64: on a 32 bit platform len(data)*8 would wrap and
Expand Down
8 changes: 8 additions & 0 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func NewReaderDict(d *Dictionary, r io.Reader) *Reader {
// The decompressed slice must be preallocated to fit the decompressed data.
// Read is the size that was decompressed and written into the 'decompressed' slice.
func (r *Reader) Read(decompressed []byte) (read int, err error) {
if r == nil {
return 0, fmt.Errorf("%w: reader is nil", ErrHuffmanDecompress)
}
if len(decompressed) == 0 {
if r.terminalErr != nil {
return 0, r.terminalErr
Expand All @@ -59,6 +62,11 @@ func (r *Reader) Read(decompressed []byte) (read int, err error) {
if r.terminalErr != nil {
return 0, r.terminalErr
}
if !r.d.isInitialized() {
err = fmt.Errorf("%w: dictionary is nil or uninitialized", ErrHuffmanDecompress)
r.terminalErr = err
return 0, err
}
if r.d.maxCodeLen > maxStoredCodeBits {
err = fmt.Errorf("%w: dictionary contains %d-bit codes, maximum supported is %d", ErrHuffmanDecompress, r.d.maxCodeLen, maxStoredCodeBits)
r.terminalErr = err
Expand Down
147 changes: 147 additions & 0 deletions regression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,153 @@ func TestUnsupportedDeepDictionaryIsRejected(t *testing.T) {
}
}

func TestNilAndZeroValueDictionariesAreRejected(t *testing.T) {
for name, dict := range map[string]*Dictionary{
"nil": nil,
"zero": new(Dictionary),
} {
t.Run(name, func(t *testing.T) {
huff := NewHuffmanDict(dict)
for _, data := range [][]byte{nil, []byte("payload")} {
if _, err := huff.Compress(data); !errors.Is(err, ErrHuffmanCompress) {
t.Errorf("Compress(%d bytes) error = %v, want ErrHuffmanCompress", len(data), err)
}
if _, err := huff.Decompress(data); !errors.Is(err, ErrHuffmanDecompress) {
t.Errorf("Decompress(%d bytes) error = %v, want ErrHuffmanDecompress", len(data), err)
}
if _, err := huff.DecompressTo(nil, data); !errors.Is(err, ErrHuffmanDecompress) {
t.Errorf("DecompressTo(%d bytes) error = %v, want ErrHuffmanDecompress", len(data), err)
}
}

var compressed bytes.Buffer
if n, err := NewWriterDict(dict, &compressed).Write([]byte("payload")); n != 0 || !errors.Is(err, ErrHuffmanCompress) {
t.Errorf("Writer.Write = (%d, %v), want (0, ErrHuffmanCompress)", n, err)
}
if n, err := NewReaderDict(dict, bytes.NewReader([]byte{0})).Read(make([]byte, 1)); n != 0 || !errors.Is(err, ErrHuffmanDecompress) {
t.Errorf("Reader.Read = (%d, %v), want (0, ErrHuffmanDecompress)", n, err)
}
})
}

var nilHuffman *Huffman
if _, err := nilHuffman.Compress(nil); !errors.Is(err, ErrHuffmanCompress) {
t.Errorf("nil Huffman.Compress error = %v, want ErrHuffmanCompress", err)
}
if _, err := nilHuffman.Decompress(nil); !errors.Is(err, ErrHuffmanDecompress) {
t.Errorf("nil Huffman.Decompress error = %v, want ErrHuffmanDecompress", err)
}
var nilWriter *Writer
if _, err := nilWriter.Write(nil); !errors.Is(err, ErrHuffmanCompress) {
t.Errorf("nil Writer.Write error = %v, want ErrHuffmanCompress", err)
}
var nilReader *Reader
if _, err := nilReader.Read(make([]byte, 1)); !errors.Is(err, ErrHuffmanDecompress) {
t.Errorf("nil Reader.Read error = %v, want ErrHuffmanDecompress", err)
}
if _, err := CompressDict(nil, nil); !errors.Is(err, ErrHuffmanCompress) {
t.Errorf("CompressDict(nil) error = %v, want ErrHuffmanCompress", err)
}
if _, err := DecompressDict(nil, nil); !errors.Is(err, ErrHuffmanDecompress) {
t.Errorf("DecompressDict(nil) error = %v, want ErrHuffmanDecompress", err)
}
}

func TestCopiedDictionaryRemainsUsable(t *testing.T) {
dict := *NewDictionary()
huff := NewHuffmanDict(&dict)
payload := snapshotLike(61, 1400)
compressed, err := huff.Compress(payload)
if err != nil {
t.Fatal(err)
}
decompressed, err := huff.Decompress(compressed)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(decompressed, payload) {
t.Fatal("copied dictionary roundtrip mismatch")
}
}

func TestDecompressToSupportsOverlappingInput(t *testing.T) {
huff := NewHuffman()
payloads := [][]byte{
bytes.Repeat([]byte{0}, 64<<10),
bytes.Repeat([]byte{1}, 64<<10),
snapshotLike(62, 64<<10),
randomBytes(63, 64<<10),
}
tests := []struct {
name string
srcOffset int
dstOffset int
dstLen int
}{
{"same-start", 0, 0, 0},
{"output-before-input", 64, 0, 8},
{"input-before-output", 0, 64, 0},
}

for payloadIndex, payload := range payloads {
compressed, err := huff.Compress(payload)
if err != nil {
t.Fatal(err)
}
for _, tt := range tests {
t.Run(itoa(payloadIndex)+"/"+tt.name, func(t *testing.T) {
storageLen := tt.srcOffset + len(compressed)
if needed := tt.dstOffset + tt.dstLen + len(payload); storageLen < needed {
storageLen = needed
}
storage := make([]byte, storageLen)
for i := 0; i < tt.dstLen; i++ {
storage[tt.dstOffset+i] = byte(0xa0 + i)
}
prefix := append([]byte(nil), storage[tt.dstOffset:tt.dstOffset+tt.dstLen]...)
copy(storage[tt.srcOffset:], compressed)

dst := storage[tt.dstOffset : tt.dstOffset+tt.dstLen]
data := storage[tt.srcOffset : tt.srcOffset+len(compressed)]
got, err := huff.DecompressTo(dst, data)
if err != nil {
t.Fatal(err)
}
want := append(prefix, payload...)
if !bytes.Equal(got, want) {
t.Fatalf("got %d bytes, want %d", len(got), len(want))
}
})
}
}
}

func TestByteSlicesOverlap(t *testing.T) {
buf := make([]byte, 32)
other := make([]byte, 32)
tests := []struct {
name string
a []byte
b []byte
want bool
}{
{"same", buf, buf, true},
{"contained", buf[4:20], buf[8:12], true},
{"partial", buf[:16], buf[8:24], true},
{"reverse-partial", buf[8:24], buf[:16], true},
{"adjacent", buf[:16], buf[16:], false},
{"separate", buf, other, false},
{"empty", buf[:0], buf, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := byteSlicesOverlap(tt.a, tt.b); got != tt.want {
t.Fatalf("byteSlicesOverlap = %v, want %v", got, tt.want)
}
})
}
}

// TestWriterMatchesCompress: the streaming Writer must emit exactly what the
// one-shot Compress emits.
func TestWriterMatchesCompress(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ func (w *Writer) Reset(rw io.Writer) {
// Write compresses the passed data and writes it to the underlying writer.
// The returned value is the number of uncompressed bytes that were written.
func (w *Writer) Write(data []byte) (written int, err error) {
if w == nil {
return 0, fmt.Errorf("%w: writer is nil", ErrHuffmanCompress)
}
if !w.d.isInitialized() {
return 0, fmt.Errorf("%w: dictionary is nil or uninitialized", ErrHuffmanCompress)
}
d := w.d

// Dictionary codes are stored as uint32. Reject deeper custom trees rather
Expand Down
Loading