diff --git a/.travis.yml b/.travis.yml index b2ab17a917..a02999a764 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ go: - 1.7.x - 1.8.x - 1.9.x + - 1.10.x - master install: diff --git a/README.md b/README.md index b35755d46d..8e5e13cf48 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ It offers slightly better compression at lower compression settings, and up to 3 [![Sourcegraph Badge](https://sourcegraph.com/github.com/klauspost/compress/-/badge.svg)](https://sourcegraph.com/github.com/klauspost/compress?badge) # changelog + +* Apr 2, 2018: Added [huff0](https://godoc.org/github.com/klauspost/compress/huff0) en/decoder. Experimental for now, API may change. +* Mar 4, 2018: Added [FSE Entropy](https://godoc.org/github.com/klauspost/compress/fse) en/decoder. Experimental for now, API may change. * Nov 3, 2017: Add compression [Estimate](https://godoc.org/github.com/klauspost/compress#Estimate) function. * May 28, 2017: Reduce allocations when resetting decoder. * Apr 02, 2017: Change back to official crc32, since changes were merged in Go 1.7. diff --git a/fse/fse.go b/fse/fse.go index 677cda77c7..57cfda30f4 100644 --- a/fse/fse.go +++ b/fse/fse.go @@ -82,17 +82,22 @@ type Scratch struct { // Histogram allows to populate the histogram and skip that step in the compression, // It otherwise allows to inspect the histogram when compression is done. -// To indicate that you have populated the histogram call the returned function +// To indicate that you have populated the histogram call HistogramFinished // with the value of the highest populated symbol, as well as the number of entries // in the most populated entry. These are accepted at face value. // The returned slice will always be length 256. -// If you have *not* populated the histogram, send 0 as maxCount before next compression cycle. -func (s *Scratch) Histogram() ([]uint32, func(maxSymbol uint8, maxCount int)) { - return s.count[:], func(maxSymbol uint8, maxCount int) { - s.maxCount = maxCount - s.symbolLen = uint16(maxSymbol) + 1 - s.clearCount = maxCount != 0 - } +func (s *Scratch) Histogram() []uint32 { + return s.count[:] +} + +// HistogramFinished can be called to indicate that the histogram has been populated. +// maxSymbol is the index of the highest set symbol of the next data segment. +// maxCount is the number of entries in the most populated entry. +// These are accepted at face value. +func (s *Scratch) HistogramFinished(maxSymbol uint8, maxCount int) { + s.maxCount = maxCount + s.symbolLen = uint16(maxSymbol) + 1 + s.clearCount = maxCount != 0 } // prepare will prepare and allocate scratch tables used for both compression and decompression. diff --git a/huff0/.gitignore b/huff0/.gitignore new file mode 100644 index 0000000000..b3d262958f --- /dev/null +++ b/huff0/.gitignore @@ -0,0 +1 @@ +/huff0-fuzz.zip diff --git a/huff0/bitreader.go b/huff0/bitreader.go new file mode 100644 index 0000000000..7d0903c701 --- /dev/null +++ b/huff0/bitreader.go @@ -0,0 +1,115 @@ +// Copyright 2018 Klaus Post. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// Based on work Copyright (c) 2013, Yann Collet, released under BSD License. + +package huff0 + +import ( + "errors" + "io" +) + +// bitReader reads a bitstream in reverse. +// The last set bit indicates the start of the stream and is used +// for aligning the input. +type bitReader struct { + in []byte + off uint // next byte to read is at in[off - 1] + value uint64 + bitsRead uint8 +} + +// init initializes and resets the bit reader. +func (b *bitReader) init(in []byte) error { + if len(in) < 1 { + return errors.New("corrupt stream: too short") + } + b.in = in + b.off = uint(len(in)) + // The highest bit of the last byte indicates where to start + v := in[len(in)-1] + if v == 0 { + return errors.New("corrupt stream, did not find end of stream") + } + b.bitsRead = 64 + b.value = 0 + b.fill() + b.fill() + b.bitsRead += 8 - uint8(highBit32(uint32(v))) + return nil +} + +// getBits will return n bits. n can be 0. +func (b *bitReader) getBits(n uint8) uint16 { + if n == 0 || b.bitsRead >= 64 { + return 0 + } + return b.getBitsFast(n) +} + +// getBitsFast requires that at least one bit is requested every time. +// There are no checks if the buffer is filled. +func (b *bitReader) getBitsFast(n uint8) uint16 { + const regMask = 64 - 1 + v := uint16((b.value << (b.bitsRead & regMask)) >> ((regMask + 1 - n) & regMask)) + b.bitsRead += n + return v +} + +// peekBitsFast requires that at least one bit is requested every time. +// There are no checks if the buffer is filled. +func (b *bitReader) peekBitsFast(n uint8) uint16 { + const regMask = 64 - 1 + v := uint16((b.value << (b.bitsRead & regMask)) >> ((regMask + 1 - n) & regMask)) + return v +} + +// fillFast() will make sure at least 32 bits are available. +// There must be at least 4 bytes available. +func (b *bitReader) fillFast() { + if b.bitsRead < 32 { + return + } + // Do single re-slice to avoid bounds checks. + v := b.in[b.off-4 : b.off] + low := (uint32(v[0])) | (uint32(v[1]) << 8) | (uint32(v[2]) << 16) | (uint32(v[3]) << 24) + b.value = (b.value << 32) | uint64(low) + b.bitsRead -= 32 + b.off -= 4 +} + +// fill() will make sure at least 32 bits are available. +func (b *bitReader) fill() { + if b.bitsRead < 32 { + return + } + if b.off > 4 { + v := b.in[b.off-4 : b.off] + low := (uint32(v[0])) | (uint32(v[1]) << 8) | (uint32(v[2]) << 16) | (uint32(v[3]) << 24) + b.value = (b.value << 32) | uint64(low) + b.bitsRead -= 32 + b.off -= 4 + return + } + for b.off > 0 { + b.value = (b.value << 8) | uint64(b.in[b.off-1]) + b.bitsRead -= 8 + b.off-- + } +} + +// finished returns true if all bits have been read from the bit stream. +func (b *bitReader) finished() bool { + return b.off == 0 && b.bitsRead >= 64 +} + +// close the bitstream and returns an error if out-of-buffer reads occurred. +func (b *bitReader) close() error { + // Release reference. + b.in = nil + if b.bitsRead > 64 { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/huff0/bitwriter.go b/huff0/bitwriter.go new file mode 100644 index 0000000000..c9d8e0ef92 --- /dev/null +++ b/huff0/bitwriter.go @@ -0,0 +1,176 @@ +// Copyright 2018 Klaus Post. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// Based on work Copyright (c) 2013, Yann Collet, released under BSD License. + +package huff0 + +import "fmt" + +// bitWriter will write bits. +// First bit will be LSB of the first byte of output. +type bitWriter struct { + bitContainer uint64 + nBits uint8 + out []byte +} + +// bitMask16 is bitmasks. Has extra to avoid bounds check. +var bitMask16 = [32]uint16{ + 0, 1, 3, 7, 0xF, 0x1F, + 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, + 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF} /* up to 16 bits */ + +// addBits16NC will add up to 16 bits. +// It will not check if there is space for them, +// so the caller must ensure that it has flushed recently. +func (b *bitWriter) addBits16NC(value uint16, bits uint8) { + b.bitContainer |= uint64(value&bitMask16[bits&31]) << (b.nBits & 63) + b.nBits += bits +} + +// addBits16Clean will add up to 16 bits. value may not contain more set bits than indicated. +// It will not check if there is space for them, so the caller must ensure that it has flushed recently. +func (b *bitWriter) addBits16Clean(value uint16, bits uint8) { + b.bitContainer |= uint64(value) << (b.nBits & 63) + b.nBits += bits +} + +// addBits16Clean will add up to 16 bits. value may not contain more set bits than indicated. +// It will not check if there is space for them, so the caller must ensure that it has flushed recently. +func (b *bitWriter) encSymbol(ct cTable, symbol byte) { + enc := ct[symbol] + b.bitContainer |= uint64(enc.val) << (b.nBits & 63) + b.nBits += enc.nBits +} + +// addBits16ZeroNC will add up to 16 bits. +// It will not check if there is space for them, +// so the caller must ensure that it has flushed recently. +// This is fastest if bits can be zero. +func (b *bitWriter) addBits16ZeroNC(value uint16, bits uint8) { + if bits == 0 { + return + } + value <<= (16 - bits) & 15 + value >>= (16 - bits) & 15 + b.bitContainer |= uint64(value) << (b.nBits & 63) + b.nBits += bits +} + +// flush will flush all pending full bytes. +// There will be at least 56 bits available for writing when this has been called. +// Using flush32 is faster, but leaves less space for writing. +func (b *bitWriter) flush() { + v := b.nBits >> 3 + switch v { + case 0: + case 1: + b.out = append(b.out, + byte(b.bitContainer), + ) + case 2: + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + ) + case 3: + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + byte(b.bitContainer>>16), + ) + case 4: + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + byte(b.bitContainer>>16), + byte(b.bitContainer>>24), + ) + case 5: + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + byte(b.bitContainer>>16), + byte(b.bitContainer>>24), + byte(b.bitContainer>>32), + ) + case 6: + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + byte(b.bitContainer>>16), + byte(b.bitContainer>>24), + byte(b.bitContainer>>32), + byte(b.bitContainer>>40), + ) + case 7: + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + byte(b.bitContainer>>16), + byte(b.bitContainer>>24), + byte(b.bitContainer>>32), + byte(b.bitContainer>>40), + byte(b.bitContainer>>48), + ) + case 8: + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + byte(b.bitContainer>>16), + byte(b.bitContainer>>24), + byte(b.bitContainer>>32), + byte(b.bitContainer>>40), + byte(b.bitContainer>>48), + byte(b.bitContainer>>56), + ) + default: + panic(fmt.Errorf("bits (%d) > 64", b.nBits)) + } + b.bitContainer >>= v << 3 + b.nBits &= 7 +} + +// flush32 will flush out, so there are at least 32 bits available for writing. +func (b *bitWriter) flush32() { + if b.nBits < 32 { + return + } + b.out = append(b.out, + byte(b.bitContainer), + byte(b.bitContainer>>8), + byte(b.bitContainer>>16), + byte(b.bitContainer>>24)) + b.nBits -= 32 + b.bitContainer >>= 32 +} + +// flushAlign will flush remaining full bytes and align to next byte boundary. +func (b *bitWriter) flushAlign() { + nbBytes := (b.nBits + 7) >> 3 + for i := uint8(0); i < nbBytes; i++ { + b.out = append(b.out, byte(b.bitContainer>>(i*8))) + } + b.nBits = 0 + b.bitContainer = 0 +} + +// close will write the alignment bit and write the final byte(s) +// to the output. +func (b *bitWriter) close() error { + // End mark + b.addBits16Clean(1, 1) + // flush until next byte. + b.flushAlign() + return nil +} + +// reset and continue writing by appending to out. +func (b *bitWriter) reset(out []byte) { + b.bitContainer = 0 + b.nBits = 0 + b.out = out +} diff --git a/huff0/bytereader.go b/huff0/bytereader.go new file mode 100644 index 0000000000..50bcdf6ea9 --- /dev/null +++ b/huff0/bytereader.go @@ -0,0 +1,54 @@ +// Copyright 2018 Klaus Post. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// Based on work Copyright (c) 2013, Yann Collet, released under BSD License. + +package huff0 + +// byteReader provides a byte reader that reads +// little endian values from a byte stream. +// The input stream is manually advanced. +// The reader performs no bounds checks. +type byteReader struct { + b []byte + off int +} + +// init will initialize the reader and set the input. +func (b *byteReader) init(in []byte) { + b.b = in + b.off = 0 +} + +// advance the stream b n bytes. +func (b *byteReader) advance(n uint) { + b.off += int(n) +} + +// Int32 returns a little endian int32 starting at current offset. +func (b byteReader) Int32() int32 { + v3 := int32(b.b[b.off+3]) + v2 := int32(b.b[b.off+2]) + v1 := int32(b.b[b.off+1]) + v0 := int32(b.b[b.off]) + return (v3 << 24) | (v2 << 16) | (v1 << 8) | v0 +} + +// Uint32 returns a little endian uint32 starting at current offset. +func (b byteReader) Uint32() uint32 { + v3 := uint32(b.b[b.off+3]) + v2 := uint32(b.b[b.off+2]) + v1 := uint32(b.b[b.off+1]) + v0 := uint32(b.b[b.off]) + return (v3 << 24) | (v2 << 16) | (v1 << 8) | v0 +} + +// unread returns the unread portion of the input. +func (b byteReader) unread() []byte { + return b.b[b.off:] +} + +// remain will return the number of bytes remaining. +func (b byteReader) remain() int { + return len(b.b) - b.off +} diff --git a/huff0/compress.go b/huff0/compress.go new file mode 100644 index 0000000000..f51a542203 --- /dev/null +++ b/huff0/compress.go @@ -0,0 +1,595 @@ +package huff0 + +import ( + "fmt" + "runtime" + "sync" +) + +func Compress1X(in []byte, s *Scratch) (out []byte, reUsed bool, err error) { + s, err = s.prepare(in) + if err != nil { + return nil, false, err + } + return compress(in, s, s.compress1X) +} + +func Compress4X(in []byte, s *Scratch) (out []byte, reUsed bool, err error) { + s, err = s.prepare(in) + if err != nil { + return nil, false, err + } + if false { + // TODO: compress4Xp only slightly faster. + const parallelThreshold = 8 << 10 + if len(in) < parallelThreshold || runtime.GOMAXPROCS(0) == 1 { + return compress(in, s, s.compress4X) + } + return compress(in, s, s.compress4Xp) + } + return compress(in, s, s.compress4X) +} + +func compress(in []byte, s *Scratch, compressor func(src []byte) ([]byte, error)) (out []byte, reUsed bool, err error) { + // Nuke previous table if we cannot reuse anyway. + if s.Reuse == ReusePolicyNone { + s.prevTable = s.prevTable[:0] + } + // Create histogram, if none was provided. + maxCount := s.maxCount + var canReuse = false + if maxCount == 0 { + maxCount, canReuse = s.countSimple(in) + } else { + canReuse = s.canUseTable(s.prevTable) + } + + // Reset for next run. + s.clearCount = true + s.maxCount = 0 + if maxCount >= len(in) { + if maxCount > len(in) { + return nil, false, fmt.Errorf("maxCount (%d) > length (%d)", maxCount, len(in)) + } + // One symbol, use RLE + return nil, false, ErrUseRLE + } + if maxCount == 1 || maxCount < (len(in)>>7) { + // Each symbol present maximum once or too well distributed. + return nil, false, ErrIncompressible + } + + if s.Reuse == ReusePolicyPrefer && canReuse { + keepTable := s.cTable + s.cTable = s.prevTable + s.Out, err = compressor(in) + s.cTable = keepTable + if err == nil && len(s.Out) < len(in) { + s.OutData = s.Out + return s.Out, true, nil + } + // Do not attempt to re-use later. + s.prevTable = s.prevTable[:0] + } + + // Calculate new table. + s.optimalTableLog() + err = s.buildCTable() + if err != nil { + return nil, false, err + } + + if !s.canUseTable(s.cTable) { + // TODO: Not needed. + panic("invalid table generated") + } + + if s.Reuse == ReusePolicyAllow && canReuse { + hSize := len(s.Out) + oldSize := s.prevTable.estimateSize(s.count[:s.symbolLen]) + newSize := s.cTable.estimateSize(s.count[:s.symbolLen]) + if oldSize <= hSize+newSize || hSize+12 >= len(in) { + // Retain cTable even if we re-use. + keepTable := s.cTable + s.cTable = s.prevTable + s.Out, err = compressor(in) + s.cTable = keepTable + if len(s.Out) >= len(in) { + return nil, false, ErrIncompressible + } + s.OutData = s.Out + return s.Out, true, nil + } + } + + // Use new table + s.cTable.write(s) + s.OutTable = s.Out + + // Compress using new table + s.Out, err = compressor(in) + if err != nil { + s.OutTable = nil + return nil, false, err + } + if len(s.Out) >= len(in) { + s.OutTable = nil + return nil, false, ErrIncompressible + } + // Move current table into previous. + s.prevTable, s.cTable = s.cTable, s.prevTable[:0] + s.OutData = s.Out[len(s.OutTable):] + return s.Out, false, nil +} + +func (s *Scratch) compress1X(src []byte) ([]byte, error) { + return s.compress1xDo(s.Out, src) +} + +func (s *Scratch) compress1xDo(dst, src []byte) ([]byte, error) { + var bw = bitWriter{out: dst} + + // N is length divisible by 4. + n := len(src) + n -= n & 3 + cTable := s.cTable[:256] + + // Encode last bytes. + for i := len(src) & 3; i > 0; i-- { + bw.encSymbol(cTable, src[n+i-1]) + } + if s.actualTableLog <= 8 { + n -= 4 + for ; n >= 0; n -= 4 { + tmp := src[n : n+4] + // tmp should be len 4 + bw.flush32() + bw.encSymbol(cTable, tmp[3]) + bw.encSymbol(cTable, tmp[2]) + bw.encSymbol(cTable, tmp[1]) + bw.encSymbol(cTable, tmp[0]) + } + } else { + n -= 4 + for ; n >= 0; n -= 4 { + tmp := src[n : n+4] + // tmp should be len 4 + bw.flush32() + bw.encSymbol(cTable, tmp[3]) + bw.encSymbol(cTable, tmp[2]) + bw.flush32() + bw.encSymbol(cTable, tmp[1]) + bw.encSymbol(cTable, tmp[0]) + } + } + err := bw.close() + return bw.out, err +} + +var sixZeros [6]byte + +func (s *Scratch) compress4X(src []byte) ([]byte, error) { + if len(src) < 12 { + return nil, ErrIncompressible + } + segmentSize := (len(src) + 3) / 4 + + // Add placeholder for output length + offsetIdx := len(s.Out) + s.Out = append(s.Out, sixZeros[:]...) + + for i := 0; i < 4; i++ { + toDo := src + if len(toDo) > segmentSize { + toDo = toDo[:segmentSize] + } + src = src[len(toDo):] + + var err error + idx := len(s.Out) + s.Out, err = s.compress1xDo(s.Out, toDo) + if err != nil { + return nil, err + } + // Write compressed length as little endian before block. + if i < 3 { + // Last length is not written. + length := len(s.Out) - idx + s.Out[i*2+offsetIdx] = byte(length) + s.Out[i*2+offsetIdx+1] = byte(length >> 8) + } + } + + return s.Out, nil +} + +// compress4Xp will compress 4 streams using separate goroutines. +func (s *Scratch) compress4Xp(src []byte) ([]byte, error) { + if len(src) < 12 { + return nil, ErrIncompressible + } + // Add placeholder for output length + s.Out = s.Out[:6] + + segmentSize := (len(src) + 3) / 4 + var wg sync.WaitGroup + var errs [4]error + wg.Add(4) + for i := 0; i < 4; i++ { + toDo := src + if len(toDo) > segmentSize { + toDo = toDo[:segmentSize] + } + src = src[len(toDo):] + + // Separate goroutine for each block. + go func(i int) { + s.tmpOut[i], errs[i] = s.compress1xDo(s.tmpOut[i][:0], toDo) + wg.Done() + }(i) + } + wg.Wait() + for i := 0; i < 4; i++ { + if errs[i] != nil { + return nil, errs[i] + } + o := s.tmpOut[i] + // Write compressed length as little endian before block. + if i < 3 { + // Last length is not written. + s.Out[i*2] = byte(len(o)) + s.Out[i*2+1] = byte(len(o) >> 8) + } + + // Write output. + s.Out = append(s.Out, o...) + } + return s.Out, nil +} + +// countSimple will create a simple histogram in s.count. +// Returns the biggest count. +// Does not update s.clearCount. +func (s *Scratch) countSimple(in []byte) (max int, reuse bool) { + reuse = true + for _, v := range in { + s.count[v]++ + } + m := uint32(0) + if len(s.prevTable) > 0 { + for i, v := range s.count[:] { + if v > m { + m = v + } + if v > 0 { + s.symbolLen = uint16(i) + 1 + if i >= len(s.prevTable) { + reuse = false + } else { + if s.prevTable[i].nBits == 0 { + reuse = false + } + } + } + } + return int(m), reuse + } + for i, v := range s.count[:] { + if v > m { + m = v + } + if v > 0 { + s.symbolLen = uint16(i) + 1 + } + } + return int(m), false +} + +func (s *Scratch) canUseTable(c cTable) bool { + if len(c) < int(s.symbolLen) { + return false + } + for i, v := range s.count[:s.symbolLen] { + if v != 0 && c[i].nBits == 0 { + return false + } + } + return true +} + +// minTableLog provides the minimum logSize to safely represent a distribution. +func (s *Scratch) minTableLog() uint8 { + minBitsSrc := highBit32(uint32(s.br.remain()-1)) + 1 + minBitsSymbols := highBit32(uint32(s.symbolLen-1)) + 2 + if minBitsSrc < minBitsSymbols { + return uint8(minBitsSrc) + } + return uint8(minBitsSymbols) +} + +// optimalTableLog calculates and sets the optimal tableLog in s.actualTableLog +func (s *Scratch) optimalTableLog() { + tableLog := s.TableLog + minBits := s.minTableLog() + maxBitsSrc := uint8(highBit32(uint32(s.br.remain()-1))) - 2 + if maxBitsSrc < tableLog { + // Accuracy can be reduced + tableLog = maxBitsSrc + } + if minBits > tableLog { + tableLog = minBits + } + // Need a minimum to safely represent all symbol values + if tableLog < minTablelog { + tableLog = minTablelog + } + if tableLog > tableLogMax { + tableLog = tableLogMax + } + s.actualTableLog = tableLog +} + +type cTableEntry struct { + val uint16 + nBits uint8 + // We have 8 bits extra +} + +const huffNodesMask = huffNodesLen - 1 + +func (s *Scratch) buildCTable() error { + s.huffSort() + s.cTable = s.cTable[:s.symbolLen] + + var startNode = int16(s.symbolLen) + nonNullRank := s.symbolLen - 1 + + nodeNb := int16(startNode) + huffNode := s.nodes[1 : huffNodesLen+1] + + // This overlays the slice above, but allows "-1" index lookups. + // Different from reference implementation. + huffNode0 := s.nodes[0 : huffNodesLen+1] + + for huffNode[nonNullRank].count == 0 { + nonNullRank-- + } + + lowS := int16(nonNullRank) + nodeRoot := nodeNb + lowS - 1 + lowN := nodeNb + huffNode[nodeNb].count = huffNode[lowS].count + huffNode[lowS-1].count + huffNode[lowS].parent, huffNode[lowS-1].parent = uint16(nodeNb), uint16(nodeNb) + nodeNb++ + lowS -= 2 + for n := nodeNb; n <= nodeRoot; n++ { + huffNode[n].count = 1 << 30 + } + // fake entry, strong barrier + huffNode0[0].count = 1 << 31 + + // create parents + for nodeNb <= nodeRoot { + var n1, n2 int16 + if huffNode0[lowS+1].count < huffNode0[lowN+1].count { + n1 = lowS + lowS-- + } else { + n1 = lowN + lowN++ + } + if huffNode0[lowS+1].count < huffNode0[lowN+1].count { + n2 = lowS + lowS-- + } else { + n2 = lowN + lowN++ + } + + huffNode[nodeNb].count = huffNode0[n1+1].count + huffNode0[n2+1].count + huffNode0[n1+1].parent, huffNode0[n2+1].parent = uint16(nodeNb), uint16(nodeNb) + nodeNb++ + } + + // distribute weights (unlimited tree height) + huffNode[nodeRoot].nbBits = 0 + for n := nodeRoot - 1; n >= startNode; n-- { + huffNode[n].nbBits = huffNode[huffNode[n].parent].nbBits + 1 + } + for n := uint16(0); n <= nonNullRank; n++ { + huffNode[n].nbBits = huffNode[huffNode[n].parent].nbBits + 1 + } + s.actualTableLog = s.setMaxHeight(int(nonNullRank)) + maxNbBits := s.actualTableLog + + // fill result into tree (val, nbBits) + if maxNbBits > tableLogMax { + return fmt.Errorf("internal error: maxNbBits (%d) > tableLogMax (%d)", maxNbBits, tableLogMax) + } + var nbPerRank [tableLogMax + 1]uint16 + var valPerRank [tableLogMax + 1]uint16 + for _, v := range huffNode[:nonNullRank+1] { + nbPerRank[v.nbBits]++ + } + // determine stating value per rank + { + min := uint16(0) + for n := maxNbBits; n > 0; n-- { + // get starting value within each rank + valPerRank[n] = min + min += nbPerRank[n] + min >>= 1 + } + } + + // push nbBits per symbol, symbol order + // TODO: changed `s.symbolLen` -> `nonNullRank+1` (micro-opt) + for _, v := range huffNode[:nonNullRank+1] { + s.cTable[v.symbol].nBits = v.nbBits + } + + // assign value within rank, symbol order + for n, val := range s.cTable[:s.symbolLen] { + v := valPerRank[val.nBits] + s.cTable[n].val = v + valPerRank[val.nBits] = v + 1 + } + + return nil +} + +// huffSort will sort symbols, decreasing order. +func (s *Scratch) huffSort() { + type rankPos struct { + base uint32 + current uint32 + } + + // Clear nodes + nodes := s.nodes[:huffNodesLen+1] + s.nodes = nodes + nodes = nodes[1 : huffNodesLen+1] + + // Sort into buckets based on length of symbol count. + var rank [32]rankPos + for _, v := range s.count[:s.symbolLen] { + r := highBit32(v+1) & 31 + rank[r].base++ + } + for n := 30; n > 0; n-- { + rank[n-1].base += rank[n].base + } + for n := range rank[:] { + rank[n].current = rank[n].base + } + for n, c := range s.count[:s.symbolLen] { + r := (highBit32(c+1) + 1) & 31 + pos := rank[r].current + rank[r].current++ + prev := nodes[(pos-1)&huffNodesMask] + for pos > rank[r].base && c > prev.count { + nodes[pos&huffNodesMask] = prev + pos-- + prev = nodes[(pos-1)&huffNodesMask] + } + nodes[pos&huffNodesMask] = nodeElt{count: c, symbol: byte(n)} + } + return +} + +func (s *Scratch) setMaxHeight(lastNonNull int) uint8 { + maxNbBits := s.TableLog + huffNode := s.nodes[1 : huffNodesLen+1] + //huffNode = huffNode[: huffNodesLen] + + largestBits := huffNode[lastNonNull].nbBits + + // early exit : no elt > maxNbBits + if largestBits <= maxNbBits { + return largestBits + } + totalCost := int(0) + baseCost := int(1) << (largestBits - maxNbBits) + n := uint32(lastNonNull) + + for huffNode[n].nbBits > maxNbBits { + totalCost += baseCost - (1 << (largestBits - huffNode[n].nbBits)) + huffNode[n].nbBits = maxNbBits + n-- + } + // n stops at huffNode[n].nbBits <= maxNbBits + + for huffNode[n].nbBits == maxNbBits { + n-- + } + // n end at index of smallest symbol using < maxNbBits + + // renorm totalCost + totalCost >>= largestBits - maxNbBits /* note : totalCost is necessarily a multiple of baseCost */ + + /* repay normalized cost */ + { + const noSymbol = 0xF0F0F0F0 + var rankLast [tableLogMax + 2]uint32 + + for i := range rankLast[:] { + rankLast[i] = noSymbol + } + + // Get pos of last (smallest) symbol per rank + { + currentNbBits := uint8(maxNbBits) + for pos := int(n); pos >= 0; pos-- { + if huffNode[pos].nbBits >= currentNbBits { + continue + } + currentNbBits = huffNode[pos].nbBits /* < maxNbBits */ + rankLast[maxNbBits-currentNbBits] = uint32(pos) + } + } + + for totalCost > 0 { + nBitsToDecrease := uint8(highBit32(uint32(totalCost))) + 1 + + for ; nBitsToDecrease > 1; nBitsToDecrease-- { + highPos := rankLast[nBitsToDecrease] + lowPos := rankLast[nBitsToDecrease-1] + if highPos == noSymbol { + continue + } + if lowPos == noSymbol { + break + } + highTotal := huffNode[highPos].count + lowTotal := 2 * huffNode[lowPos].count + if highTotal <= lowTotal { + break + } + } + // only triggered when no more rank 1 symbol left => find closest one (note : there is necessarily at least one !) + // HUF_MAX_TABLELOG test just to please gcc 5+; but it should not be necessary + // FIXME: try to remove + for (nBitsToDecrease <= tableLogMax) && (rankLast[nBitsToDecrease] == noSymbol) { + nBitsToDecrease++ + } + totalCost -= 1 << (nBitsToDecrease - 1) + if rankLast[nBitsToDecrease-1] == noSymbol { + // this rank is no longer empty + rankLast[nBitsToDecrease-1] = rankLast[nBitsToDecrease] + } + huffNode[rankLast[nBitsToDecrease]].nbBits++ + if rankLast[nBitsToDecrease] == 0 { + /* special case, reached largest symbol */ + rankLast[nBitsToDecrease] = noSymbol + } else { + rankLast[nBitsToDecrease]-- + if huffNode[rankLast[nBitsToDecrease]].nbBits != maxNbBits-nBitsToDecrease { + rankLast[nBitsToDecrease] = noSymbol /* this rank is now empty */ + } + } + } + + for totalCost < 0 { /* Sometimes, cost correction overshoot */ + if rankLast[1] == noSymbol { /* special case : no rank 1 symbol (using maxNbBits-1); let's create one from largest rank 0 (using maxNbBits) */ + for huffNode[n].nbBits == maxNbBits { + n-- + } + huffNode[n+1].nbBits-- + rankLast[1] = n + 1 + totalCost++ + continue + } + huffNode[rankLast[1]+1].nbBits-- + rankLast[1]++ + totalCost++ + } + } + return maxNbBits +} + +type nodeElt struct { + count uint32 + parent uint16 + symbol byte + nbBits uint8 +} diff --git a/huff0/compress_test.go b/huff0/compress_test.go new file mode 100644 index 0000000000..7cc3bb1efc --- /dev/null +++ b/huff0/compress_test.go @@ -0,0 +1,437 @@ +package huff0 + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/klauspost/compress/flate" +) + +type inputFn func() ([]byte, error) + +var testfiles = []struct { + name string + fn inputFn + err1X error + err4X error +}{ + // Digits is the digits of the irrational number e. Its decimal representation + // does not repeat, but there are only 10 possible digits, so it should be + // reasonably compressible. + {name: "digits", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/e.txt") }}, + // gettysburg.txt is a small plain text. + {name: "gettysburg", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/gettysburg.txt") }}, + // Twain is Project Gutenberg's edition of Mark Twain's classic English novel. + {name: "twain", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/Mark.Twain-Tom.Sawyer.txt") }}, + // Random bytes + {name: "random", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/sharnd.out") }, err1X: ErrIncompressible, err4X: ErrIncompressible}, + // Low entropy + {name: "low-ent.10k", fn: func() ([]byte, error) { return []byte(strings.Repeat("1221", 10000)), nil }}, + // Super Low entropy + {name: "superlow-ent-10k", fn: func() ([]byte, error) { return []byte(strings.Repeat("1", 10000) + strings.Repeat("2", 500)), nil }}, + // Zero bytes + {name: "zeroes", fn: func() ([]byte, error) { return make([]byte, 10000), nil }, err1X: ErrUseRLE, err4X: ErrUseRLE}, + {name: "crash1", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/crash1.bin") }, err1X: ErrIncompressible, err4X: ErrIncompressible}, + {name: "crash2", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/crash2.bin") }, err4X: ErrIncompressible}, + {name: "crash3", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/crash3.bin") }, err1X: ErrIncompressible, err4X: ErrIncompressible}, + {name: "endzerobits", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/endzerobits.bin") }, err1X: nil, err4X: ErrIncompressible}, + {name: "endnonzero", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/endnonzero.bin") }, err4X: ErrIncompressible}, + {name: "case1", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/case1.bin") }, err1X: nil}, + {name: "case2", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/case2.bin") }, err1X: nil}, + {name: "case3", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/case3.bin") }, err1X: nil}, + {name: "pngdata.001", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/pngdata.bin") }, err1X: nil}, + {name: "normcount2", fn: func() ([]byte, error) { return ioutil.ReadFile("../testdata/normcount2.bin") }, err1X: nil}, +} + +type fuzzInput struct { + name string + fn inputFn +} + +var testfilesExtended []fuzzInput + +func init() { + filepath.Walk("./fuzz/compress/corpus", func(path string, info os.FileInfo, err error) error { + if info.Size() == 0 || info.IsDir() { + return nil + } + testfilesExtended = append(testfilesExtended, fuzzInput{ + name: filepath.Base(path), + fn: func() ([]byte, error) { + return ioutil.ReadFile(path) + }, + }) + return nil + }) +} + +func TestCompress1X(t *testing.T) { + for _, test := range testfiles { + t.Run(test.name, func(t *testing.T) { + var s Scratch + buf0, err := test.fn() + if err != nil { + t.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b, re, err := Compress1X(buf0, &s) + if err != test.err1X { + t.Errorf("want error %v (%T), got %v (%T)", test.err1X, test.err1X, err, err) + } + if err != nil { + t.Log(test.name, err.Error()) + return + } + if b == nil { + t.Error("got no output") + return + } + min := s.minSize(len(buf0)) + if len(s.OutData) < min { + t.Errorf("output data length (%d) below shannon limit (%d)", len(s.OutData), min) + } + if len(s.OutTable) == 0 { + t.Error("got no table definition") + } + if re { + t.Error("claimed to have re-used.") + } + if len(s.OutData) == 0 { + t.Error("got no data output") + } + t.Logf("%s: %d -> %d bytes (%.2f:1) re:%t (table: %d bytes)", test.name, len(buf0), len(b), float64(len(buf0))/float64(len(b)), re, len(s.OutTable)) + s.Out = nil + bRe, _, err := Compress1X(b, &s) + if err == nil { + t.Log("Could re-compress to", len(bRe)) + } + }) + } +} + +func TestCompress4X(t *testing.T) { + for _, test := range testfiles { + t.Run(test.name, func(t *testing.T) { + var s Scratch + buf0, err := test.fn() + if err != nil { + t.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b, re, err := Compress4X(buf0, &s) + if err != test.err4X { + t.Errorf("want error %v (%T), got %v (%T)", test.err1X, test.err4X, err, err) + } + if err != nil { + t.Log(test.name, err.Error()) + return + } + if b == nil { + t.Error("got no output") + return + } + if len(s.OutTable) == 0 { + t.Error("got no table definition") + } + if re { + t.Error("claimed to have re-used.") + } + if len(s.OutData) == 0 { + t.Error("got no data output") + } + + t.Logf("%s: %d -> %d bytes (%.2f:1) %t (table: %d bytes)", test.name, len(buf0), len(b), float64(len(buf0))/float64(len(b)), re, len(s.OutTable)) + }) + } +} + +func TestCompress1XReuse(t *testing.T) { + for _, test := range testfiles { + t.Run(test.name, func(t *testing.T) { + var s Scratch + buf0, err := test.fn() + if err != nil { + t.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b, re, err := Compress1X(buf0, &s) + if err != test.err1X { + t.Errorf("want error %v (%T), got %v (%T)", test.err1X, test.err1X, err, err) + } + if err != nil { + t.Log(test.name, err.Error()) + return + } + if b == nil { + t.Error("got no output") + return + } + firstData := len(s.OutData) + s.Reuse = ReusePolicyAllow + b, re, err = Compress1X(buf0, &s) + if err != nil { + t.Errorf("got secondary error %v (%T)", err, err) + return + } + if !re { + t.Error("Didn't re-use even if data was the same") + } + if len(s.OutTable) != 0 { + t.Error("got table definition, don't want any") + } + if len(s.OutData) == 0 { + t.Error("got no data output") + } + if len(b) != firstData { + t.Errorf("data length did not match first: %d, second:%d", firstData, len(b)) + } + t.Logf("%s: %d -> %d bytes (%.2f:1) %t", test.name, len(buf0), len(b), float64(len(buf0))/float64(len(b)), re) + }) + } +} + +func BenchmarkDeflate(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err1X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + dec, err := flate.NewWriter(ioutil.Discard, flate.HuffmanOnly) + if err != nil { + b.Fatal(err) + } + if test.err1X != nil { + b.Skip("skipping") + } + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + dec.Reset(ioutil.Discard) + n, err := dec.Write(buf0) + if err != nil { + b.Fatal(err) + } + if n != len(buf0) { + b.Fatal("mismatch", n, len(buf0)) + } + dec.Close() + } + }) + } +} + +func BenchmarkCompress1XReuseNone(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err1X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s Scratch + s.Reuse = ReusePolicyNone + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + _, re, err := Compress1X(buf0, &s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, re, _ = Compress1X(buf0, &s) + if re { + b.Fatal("reused") + } + } + }) + } +} + +func BenchmarkCompress1XReuseAllow(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err1X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s Scratch + s.Reuse = ReusePolicyAllow + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + _, re, err := Compress1X(buf0, &s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, re, _ = Compress1X(buf0, &s) + if !re { + b.Fatal("not reused") + } + } + }) + } +} + +func BenchmarkCompress1XReusePrefer(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err1X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s Scratch + s.Reuse = ReusePolicyPrefer + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + _, re, err := Compress1X(buf0, &s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, re, _ = Compress1X(buf0, &s) + if !re { + b.Fatal("not reused") + } + } + }) + } +} + +func BenchmarkCompress4XReuseNone(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err4X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s Scratch + s.Reuse = ReusePolicyNone + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + _, re, err := Compress4X(buf0, &s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, re, _ = Compress4X(buf0, &s) + if re { + b.Fatal("reused") + } + } + }) + } +} + +func BenchmarkCompress4XReuseAllow(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err4X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s Scratch + s.Reuse = ReusePolicyAllow + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + _, re, err := Compress4X(buf0, &s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, re, _ = Compress4X(buf0, &s) + if !re { + b.Fatal("not reused") + } + } + }) + } +} + +func BenchmarkCompress4XReusePrefer(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err4X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s Scratch + s.Reuse = ReusePolicyPrefer + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + _, re, err := Compress4X(buf0, &s) + if err != test.err4X { + b.Fatal("unexpected error:", err) + } + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, re, _ = Compress4X(buf0, &s) + if !re { + b.Fatal("not reused") + } + } + }) + } +} diff --git a/huff0/decompress.go b/huff0/decompress.go new file mode 100644 index 0000000000..436cc41312 --- /dev/null +++ b/huff0/decompress.go @@ -0,0 +1,392 @@ +package huff0 + +import ( + "errors" + "fmt" + "io" + + "github.com/klauspost/compress/fse" +) + +type dTable struct { + single []dEntrySingle + double []dEntryDouble +} + +// single-symbols decoding +type dEntrySingle struct { + byte uint8 + nBits uint8 +} + +// double-symbols decoding +type dEntryDouble struct { + seq uint16 + nBits uint8 + len uint8 +} + +// ReadTable will read a table from the input. +// The size of the input may be larger than the table definition. +// Any content remaining after the table definition will be returned. +// if no Scratch is provided a new one is allocated. +// The returned Scratch can be used for decoding input using this table. +func ReadTable(in []byte, s *Scratch) (s2 *Scratch, remain []byte, err error) { + s, err = s.prepare(in) + if err != nil { + return s, nil, err + } + if len(in) <= 1 { + return s, nil, errors.New("input too small for table") + } + iSize := in[0] + in = in[1:] + if iSize >= 128 { + // Uncompressed + oSize := iSize - 127 + iSize = (oSize + 1) / 2 + if int(iSize) > len(in) { + return s, nil, errors.New("input too small for table") + } + for n := uint8(0); n < oSize; n += 2 { + v := in[n/2] + s.huffWeight[n] = v >> 4 + s.huffWeight[n+1] = v & 15 + } + s.symbolLen = uint16(oSize) + in = in[iSize:] + } else { + if len(in) <= int(iSize) { + return s, nil, errors.New("input too small for table") + } + // FSE compressed weights + s.fse.DecompressLimit = 255 + hw := s.huffWeight[:] + s.fse.Out = hw + b, err := fse.Decompress(in[:iSize], s.fse) + s.fse.Out = nil + if err != nil { + return s, nil, err + } + if len(b) > 255 { + return s, nil, errors.New("corrupt input: output table too large") + } + s.symbolLen = uint16(len(b)) + in = in[iSize:] + } + + // collect weight stats + var rankStats [tableLogMax + 1]uint32 + weightTotal := uint32(0) + for _, v := range s.huffWeight[:s.symbolLen] { + if v > tableLogMax { + return s, nil, errors.New("corrupt input: weight too large") + } + rankStats[v]++ + weightTotal += (1 << (v & 15)) >> 1 + } + if weightTotal == 0 { + return s, nil, errors.New("corrupt input: weights zero") + } + + // get last non-null symbol weight (implied, total must be 2^n) + { + tableLog := highBit32(weightTotal) + 1 + if tableLog > tableLogMax { + return s, nil, errors.New("corrupt input: tableLog too big") + } + s.actualTableLog = uint8(tableLog) + // determine last weight + { + total := uint32(1) << tableLog + rest := total - weightTotal + verif := uint32(1) << highBit32(rest) + lastWeight := highBit32(rest) + 1 + if verif != rest { + // last value must be a clean power of 2 + return s, nil, errors.New("corrupt input: last value not power of two") + } + s.huffWeight[s.symbolLen] = uint8(lastWeight) + s.symbolLen++ + rankStats[lastWeight]++ + } + } + + if (rankStats[1] < 2) || (rankStats[1]&1 != 0) { + // by construction : at least 2 elts of rank 1, must be even + return s, nil, errors.New("corrupt input: min elt size, even check failed ") + } + + // TODO: Choose between single/double symbol decoding + + // Calculate starting value for each rank + { + var nextRankStart uint32 + for n := uint8(1); n < s.actualTableLog+1; n++ { + current := nextRankStart + nextRankStart += rankStats[n] << (n - 1) + rankStats[n] = current + } + } + + // fill DTable (always full size) + tSize := 1 << tableLogMax + if len(s.dt.single) != tSize { + s.dt.single = make([]dEntrySingle, tSize) + } + + for n, w := range s.huffWeight[:s.symbolLen] { + length := (uint32(1) << w) >> 1 + d := dEntrySingle{ + byte: uint8(n), + nBits: s.actualTableLog + 1 - w, + } + for u := rankStats[w]; u < rankStats[w]+length; u++ { + s.dt.single[u] = d + } + rankStats[w] += length + } + return s, in, nil +} + +// Decompress1X will decompress a 1X encoded stream. +// The supplied input must match the end of a block exactly. +// Before this is called, the table must be initialized with ReadTable. +func (s *Scratch) Decompress1X(in []byte) (out []byte, err error) { + if len(s.dt.single) == 0 { + return nil, errors.New("no table loaded") + } + var br bitReader + err = br.init(in) + if err != nil { + return nil, err + } + s.Out = s.Out[:0] + + decode := func() byte { + val := br.peekBitsFast(s.actualTableLog) /* note : actualTableLog >= 1 */ + v := s.dt.single[val] + br.bitsRead += v.nBits + return v.byte + } + hasDec := func(v dEntrySingle) byte { + br.bitsRead += v.nBits + return v.byte + } + + // Avoid bounds check by always having full sized table. + const tlSize = 1 << tableLogMax + const tlMask = tlSize - 1 + dt := s.dt.single[:tlSize] + + // Use temp table to avoid bound checks/append penalty. + var tmp = s.huffWeight[:256] + var off uint8 + + for br.off >= 8 { + br.fillFast() + tmp[off+0] = hasDec(dt[br.peekBitsFast(s.actualTableLog)&tlMask]) + tmp[off+1] = hasDec(dt[br.peekBitsFast(s.actualTableLog)&tlMask]) + br.fillFast() + tmp[off+2] = hasDec(dt[br.peekBitsFast(s.actualTableLog)&tlMask]) + tmp[off+3] = hasDec(dt[br.peekBitsFast(s.actualTableLog)&tlMask]) + off += 4 + if off == 0 { + s.Out = append(s.Out, tmp...) + } + } + + s.Out = append(s.Out, tmp[:off]...) + + for !br.finished() { + br.fill() + s.Out = append(s.Out, decode()) + } + return s.Out, br.close() +} + +// Decompress4X will decompress a 4X encoded stream. +// Before this is called, the table must be initialized with ReadTable. +// The supplied input must match the end of a block exactly. +// The destination size of the uncompressed data must be known and provided. +func (s *Scratch) Decompress4X(in []byte, dstSize int) (out []byte, err error) { + if len(s.dt.single) == 0 { + return nil, errors.New("no table loaded") + } + if len(in) < 6+(4*1) { + return nil, errors.New("input too small") + } + // TODO: We do not detect when we overrun a buffer, except if the last one does. + + var br [4]bitReader + start := 6 + for i := 0; i < 3; i++ { + length := int(in[i*2]) | (int(in[i*2+1]) << 8) + if start+length >= len(in) { + return nil, errors.New("truncated input (or invalid offset)") + } + err = br[i].init(in[start : start+length]) + if err != nil { + return nil, err + } + start += length + } + err = br[3].init(in[start:]) + if err != nil { + return nil, err + } + + // Prepare output + if cap(s.Out) < dstSize { + s.Out = make([]byte, 0, dstSize) + } + s.Out = s.Out[:dstSize] + // destination, offset to match first output + dstOut := s.Out + dstEvery := (dstSize + 3) / 4 + + decode := func(br *bitReader) byte { + val := br.peekBitsFast(s.actualTableLog) /* note : actualTableLog >= 1 */ + v := s.dt.single[val] + br.bitsRead += v.nBits + return v.byte + } + + // Use temp table to avoid bound checks/append penalty. + var tmp = s.huffWeight[:256] + var off uint8 + + // Decode 2 values from each decoder/loop. + const bufoff = 256 / 4 +bigloop: + for { + for i := range br { + if br[i].off < 4 { + break bigloop + } + br[i].fillFast() + } + tmp[off] = decode(&br[0]) + tmp[off+bufoff] = decode(&br[1]) + tmp[off+bufoff*2] = decode(&br[2]) + tmp[off+bufoff*3] = decode(&br[3]) + tmp[off+1] = decode(&br[0]) + tmp[off+1+bufoff] = decode(&br[1]) + tmp[off+1+bufoff*2] = decode(&br[2]) + tmp[off+1+bufoff*3] = decode(&br[3]) + off += 2 + if off == bufoff { + copy(dstOut, tmp[:bufoff]) + copy(dstOut[dstEvery:], tmp[bufoff:bufoff*2]) + copy(dstOut[dstEvery*2:], tmp[bufoff*2:bufoff*3]) + copy(dstOut[dstEvery*3:], tmp[bufoff*3:bufoff*4]) + off = 0 + dstOut = dstOut[bufoff:] + // There must at least be 3 buffers left. + if len(dstOut) < dstEvery*3+3 { + return nil, errors.New("corruption detected: stream overrun") + } + } + } + if off > 0 { + ioff := int(off) + if len(dstOut) < dstEvery*3-ioff { + return nil, errors.New("corruption detected: stream overrun") + } + copy(dstOut, tmp[:off]) + copy(dstOut[dstEvery:dstEvery+ioff], tmp[bufoff:bufoff*2]) + copy(dstOut[dstEvery*2:dstEvery*2+ioff], tmp[bufoff*2:bufoff*3]) + copy(dstOut[dstEvery*3:dstEvery*3+ioff], tmp[bufoff*3:bufoff*4]) + dstOut = dstOut[off:] + } + + for i := range br { + offset := dstEvery * i + br := &br[i] + for !br.finished() { + br.fill() + if offset >= len(dstOut) { + return nil, errors.New("corruption detected: stream overrun") + } + dstOut[offset] = decode(br) + offset++ + } + err = br.close() + if err != nil { + return nil, err + } + } + + return s.Out, nil +} + +// matches will compare a decoding table to a coding table. +// Errors are written to the writer. +// Nothing will be written if table is ok. +func (s *Scratch) matches(ct cTable, w io.Writer) { + if s == nil || len(s.dt.single) == 0 { + return + } + dt := s.dt.single[:1< 0 { + fmt.Fprintf(w, "%d errros in base, stopping\n", errs) + continue + } + // Ensure that all combinations are covered. + for i := uint16(0); i < (1 << ub); i++ { + vval := top | i + dec := dt[vval] + if dec.nBits != enc.nBits { + fmt.Fprintf(w, "symbol 0x%x bit size mismatch (enc: %d, dec:%d).\n", vval, enc.nBits, dec.nBits) + errs++ + } + if dec.byte != uint8(sym) { + fmt.Fprintf(w, "symbol 0x%x decoder output mismatch (enc: %d, dec:%d).\n", vval, sym, dec.byte) + errs++ + } + if errs > 20 { + fmt.Fprintf(w, "%d errros, stopping\n", errs) + break + } + } + if errs == 0 { + ok++ + broken-- + } + } + if broken > 0 { + fmt.Fprintf(w, "%d broken, %d ok\n", broken, ok) + } +} diff --git a/huff0/decompress_test.go b/huff0/decompress_test.go new file mode 100644 index 0000000000..579ab399cd --- /dev/null +++ b/huff0/decompress_test.go @@ -0,0 +1,482 @@ +package huff0 + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestDecompress1X(t *testing.T) { + for _, test := range testfiles { + t.Run(test.name, func(t *testing.T) { + var s = &Scratch{} + buf0, err := test.fn() + if err != nil { + t.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b, re, err := Compress1X(buf0, s) + if err != test.err1X { + t.Errorf("want error %v (%T), got %v (%T)", test.err1X, test.err1X, err, err) + } + if err != nil { + t.Log(test.name, err.Error()) + return + } + if b == nil { + t.Error("got no output") + return + } + if len(s.OutTable) == 0 { + t.Error("got no table definition") + } + if re { + t.Error("claimed to have re-used.") + } + if len(s.OutData) == 0 { + t.Error("got no data output") + } + + wantRemain := len(s.OutData) + t.Logf("%s: %d -> %d bytes (%.2f:1) %t (table: %d bytes)", test.name, len(buf0), len(b), float64(len(buf0))/float64(len(b)), re, len(s.OutTable)) + + s.Out = nil + var remain []byte + s, remain, err = ReadTable(b, s) + if err != nil { + t.Error(err) + return + } + var buf bytes.Buffer + if s.matches(s.prevTable, &buf); buf.Len() > 0 { + t.Error(buf.String()) + } + if len(remain) != wantRemain { + t.Fatalf("remain mismatch, want %d, got %d bytes", wantRemain, len(remain)) + } + t.Logf("remain: %d bytes, ok", len(remain)) + dc, err := s.Decompress1X(remain) + if err != nil { + t.Error(err) + return + } + if len(buf0) != len(dc) { + t.Errorf(test.name+"decompressed, want size: %d, got %d", len(buf0), len(dc)) + if len(buf0) > len(dc) { + buf0 = buf0[:len(dc)] + } else { + dc = dc[:len(buf0)] + } + if !cmp.Equal(buf0, dc) { + if len(dc) > 1024 { + t.Log(string(dc[:1024])) + t.Errorf(test.name+"decompressed, got delta: \n(in)\t%02x !=\n(out)\t%02x\n", buf0[:1024], dc[:1024]) + } else { + t.Log(string(dc)) + t.Errorf(test.name+"decompressed, got delta: (in) %v != (out) %v\n", buf0, dc) + } + } + return + } + if !cmp.Equal(buf0, dc) { + if len(buf0) > 1024 { + t.Log(string(dc[:1024])) + } else { + t.Log(string(dc)) + } + //t.Errorf(test.name+": decompressed, got delta: \n%s") + t.Errorf(test.name + ": decompressed, got delta") + } + if !t.Failed() { + t.Log("... roundtrip ok!") + } + }) + } +} + +func TestDecompress4X(t *testing.T) { + for _, test := range testfiles { + t.Run(test.name, func(t *testing.T) { + var s = &Scratch{} + buf0, err := test.fn() + if err != nil { + t.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b, re, err := Compress4X(buf0, s) + if err != test.err4X { + t.Errorf("want error %v (%T), got %v (%T)", test.err1X, test.err1X, err, err) + } + if err != nil { + t.Log(test.name, err.Error()) + return + } + if b == nil { + t.Error("got no output") + return + } + if len(s.OutTable) == 0 { + t.Error("got no table definition") + } + if re { + t.Error("claimed to have re-used.") + } + if len(s.OutData) == 0 { + t.Error("got no data output") + } + + wantRemain := len(s.OutData) + t.Logf("%s: %d -> %d bytes (%.2f:1) %t (table: %d bytes)", test.name, len(buf0), len(b), float64(len(buf0))/float64(len(b)), re, len(s.OutTable)) + + s.Out = nil + var remain []byte + s, remain, err = ReadTable(b, s) + if err != nil { + t.Error(err) + return + } + var buf bytes.Buffer + if s.matches(s.prevTable, &buf); buf.Len() > 0 { + t.Error(buf.String()) + } + if len(remain) != wantRemain { + t.Fatalf("remain mismatch, want %d, got %d bytes", wantRemain, len(remain)) + } + t.Logf("remain: %d bytes, ok", len(remain)) + dc, err := s.Decompress4X(remain, len(buf0)) + if err != nil { + t.Error(err) + return + } + if len(buf0) != len(dc) { + t.Errorf(test.name+"decompressed, want size: %d, got %d", len(buf0), len(dc)) + if len(buf0) > len(dc) { + buf0 = buf0[:len(dc)] + } else { + dc = dc[:len(buf0)] + } + if !cmp.Equal(buf0, dc) { + if len(dc) > 1024 { + t.Log(string(dc[:1024])) + t.Errorf(test.name+"decompressed, got delta: \n(in)\t%02x !=\n(out)\t%02x\n", buf0[:1024], dc[:1024]) + } else { + t.Log(string(dc)) + t.Errorf(test.name+"decompressed, got delta: (in) %v != (out) %v\n", buf0, dc) + } + } + return + } + if !cmp.Equal(buf0, dc) { + if len(buf0) > 1024 { + t.Log(string(dc[:1024])) + } else { + t.Log(string(dc)) + } + //t.Errorf(test.name+": decompressed, got delta: \n%s") + t.Errorf(test.name + ": decompressed, got delta") + } + if !t.Failed() { + t.Log("... roundtrip ok!") + } + }) + } +} + +func TestDecompress1XFuzz(t *testing.T) { + for _, test := range testfilesExtended { + t.Run(test.name, func(t *testing.T) { + var s = &Scratch{} + buf0, err := test.fn() + if err != nil { + t.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b, re, err := Compress1X(buf0, s) + if err != nil { + if err == ErrIncompressible || err == ErrUseRLE || err == ErrTooBig { + t.Log(test.name, err.Error()) + return + } + t.Error(test.name, err.Error()) + return + } + if b == nil { + t.Error("got no output") + return + } + if len(s.OutTable) == 0 { + t.Error("got no table definition") + } + if re { + t.Error("claimed to have re-used.") + } + if len(s.OutData) == 0 { + t.Error("got no data output") + } + + wantRemain := len(s.OutData) + t.Logf("%s: %d -> %d bytes (%.2f:1) %t (table: %d bytes)", test.name, len(buf0), len(b), float64(len(buf0))/float64(len(b)), re, len(s.OutTable)) + + s.Out = nil + var remain []byte + s, remain, err = ReadTable(b, s) + if err != nil { + t.Error(err) + return + } + var buf bytes.Buffer + if s.matches(s.prevTable, &buf); buf.Len() > 0 { + t.Error(buf.String()) + } + if len(remain) != wantRemain { + t.Fatalf("remain mismatch, want %d, got %d bytes", wantRemain, len(remain)) + } + t.Logf("remain: %d bytes, ok", len(remain)) + dc, err := s.Decompress1X(remain) + if err != nil { + t.Error(err) + return + } + if len(buf0) != len(dc) { + t.Errorf(test.name+"decompressed, want size: %d, got %d", len(buf0), len(dc)) + if len(buf0) > len(dc) { + buf0 = buf0[:len(dc)] + } else { + dc = dc[:len(buf0)] + } + if !cmp.Equal(buf0, dc) { + if len(dc) > 1024 { + t.Log(string(dc[:1024])) + t.Errorf(test.name+"decompressed, got delta: \n(in)\t%02x !=\n(out)\t%02x\n", buf0[:1024], dc[:1024]) + } else { + t.Log(string(dc)) + t.Errorf(test.name+"decompressed, got delta: (in) %v != (out) %v\n", buf0, dc) + } + } + return + } + if !cmp.Equal(buf0, dc) { + if len(buf0) > 1024 { + t.Log(string(dc[:1024])) + } else { + t.Log(string(dc)) + } + //t.Errorf(test.name+": decompressed, got delta: \n%s") + t.Errorf(test.name + ": decompressed, got delta") + } + if !t.Failed() { + t.Log("... roundtrip ok!") + } + }) + } +} + +func TestDecompress4XFuzz(t *testing.T) { + for _, test := range testfilesExtended { + t.Run(test.name, func(t *testing.T) { + var s = &Scratch{} + buf0, err := test.fn() + if err != nil { + t.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + b, re, err := Compress4X(buf0, s) + if err != nil { + if err == ErrIncompressible || err == ErrUseRLE || err == ErrTooBig { + t.Log(test.name, err.Error()) + return + } + t.Error(test.name, err.Error()) + return + } + if b == nil { + t.Error("got no output") + return + } + if len(s.OutTable) == 0 { + t.Error("got no table definition") + } + if re { + t.Error("claimed to have re-used.") + } + if len(s.OutData) == 0 { + t.Error("got no data output") + } + + wantRemain := len(s.OutData) + t.Logf("%s: %d -> %d bytes (%.2f:1) %t (table: %d bytes)", test.name, len(buf0), len(b), float64(len(buf0))/float64(len(b)), re, len(s.OutTable)) + + s.Out = nil + var remain []byte + s, remain, err = ReadTable(b, s) + if err != nil { + t.Error(err) + return + } + var buf bytes.Buffer + if s.matches(s.prevTable, &buf); buf.Len() > 0 { + t.Error(buf.String()) + } + if len(remain) != wantRemain { + t.Fatalf("remain mismatch, want %d, got %d bytes", wantRemain, len(remain)) + } + t.Logf("remain: %d bytes, ok", len(remain)) + dc, err := s.Decompress4X(remain, len(buf0)) + if err != nil { + t.Error(err) + return + } + if len(buf0) != len(dc) { + t.Errorf(test.name+"decompressed, want size: %d, got %d", len(buf0), len(dc)) + if len(buf0) > len(dc) { + buf0 = buf0[:len(dc)] + } else { + dc = dc[:len(buf0)] + } + if !cmp.Equal(buf0, dc) { + if len(dc) > 1024 { + t.Log(string(dc[:1024])) + t.Errorf(test.name+"decompressed, got delta: \n(in)\t%02x !=\n(out)\t%02x\n", buf0[:1024], dc[:1024]) + } else { + t.Log(string(dc)) + t.Errorf(test.name+"decompressed, got delta: (in) %v != (out) %v\n", buf0, dc) + } + } + return + } + if !cmp.Equal(buf0, dc) { + if len(buf0) > 1024 { + t.Log(string(dc[:1024])) + } else { + t.Log(string(dc)) + } + //t.Errorf(test.name+": decompressed, got delta: \n%s") + t.Errorf(test.name + ": decompressed, got delta") + } + if !t.Failed() { + t.Log("... roundtrip ok!") + } + }) + } +} + +func BenchmarkDecompress1XTable(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err1X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s = &Scratch{} + s.Reuse = ReusePolicyNone + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + compressed, _, err := Compress1X(buf0, s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + s.Out = nil + s, remain, _ := ReadTable(compressed, s) + s.Decompress1X(remain) + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + s, remain, err := ReadTable(compressed, s) + if err != nil { + b.Fatal(err) + } + _, err = s.Decompress1X(remain) + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkDecompress1XNoTable(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err1X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s = &Scratch{} + s.Reuse = ReusePolicyNone + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + compressed, _, err := Compress1X(buf0, s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + s.Out = nil + s, remain, _ := ReadTable(compressed, s) + s.Decompress1X(remain) + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, err = s.Decompress1X(remain) + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkDecompress4XNoTable(b *testing.B) { + for _, tt := range testfiles { + test := tt + if test.err4X != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + var s = &Scratch{} + s.Reuse = ReusePolicyNone + buf0, err := test.fn() + if err != nil { + b.Fatal(err) + } + if len(buf0) > BlockSizeMax { + buf0 = buf0[:BlockSizeMax] + } + compressed, _, err := Compress4X(buf0, s) + if err != test.err1X { + b.Fatal("unexpected error:", err) + } + s.Out = nil + s, remain, _ := ReadTable(compressed, s) + s.Decompress4X(remain, len(buf0)) + b.ResetTimer() + b.ReportAllocs() + b.SetBytes(int64(len(buf0))) + for i := 0; i < b.N; i++ { + _, err = s.Decompress4X(remain, len(buf0)) + if err != nil { + b.Fatal(err) + } + } + }) + } +} diff --git a/huff0/fuzz/compress/corpus/01ace8efdfd46d82b0fef511b259933ced9d13ec-2 b/huff0/fuzz/compress/corpus/01ace8efdfd46d82b0fef511b259933ced9d13ec-2 new file mode 100644 index 0000000000..69291ec93e --- /dev/null +++ b/huff0/fuzz/compress/corpus/01ace8efdfd46d82b0fef511b259933ced9d13ec-2 @@ -0,0 +1 @@ +012939495969798997C2 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/049f4a878ab48c404bd0950e2366e3c0eaad4318-3 b/huff0/fuzz/compress/corpus/049f4a878ab48c404bd0950e2366e3c0eaad4318-3 new file mode 100644 index 0000000000..84e824506e Binary files /dev/null and b/huff0/fuzz/compress/corpus/049f4a878ab48c404bd0950e2366e3c0eaad4318-3 differ diff --git a/huff0/fuzz/compress/corpus/04bc4648208f40ba61ffb9a929f760c24101522c-1 b/huff0/fuzz/compress/corpus/04bc4648208f40ba61ffb9a929f760c24101522c-1 new file mode 100644 index 0000000000..3494057c2e Binary files /dev/null and b/huff0/fuzz/compress/corpus/04bc4648208f40ba61ffb9a929f760c24101522c-1 differ diff --git a/huff0/fuzz/compress/corpus/0a7ce59229caf93ee64d5a828aecab7b5e434bbc b/huff0/fuzz/compress/corpus/0a7ce59229caf93ee64d5a828aecab7b5e434bbc new file mode 100644 index 0000000000..80da8c0d0b --- /dev/null +++ b/huff0/fuzz/compress/corpus/0a7ce59229caf93ee64d5a828aecab7b5e434bbc @@ -0,0 +1 @@ +CaDCaDcAecDc7De7777-accD77DeDEEAf_ZohDnpPnXJKmFtxmGZFOVgbxbCaAEEAAfecbD4dc44.x.xAfF-45405050xdbCaDAEEd0EcdDA7AfecbD4dc454057025Vz-04454224054e-0276666767011776664.efZohQpggnpGPTrniXRqJlKmFtmGZFOgVMgwxAAfeB643xbBCaBABE3BEcd3AAfecbD64Bdc664423703235e-07503-0xB66C9fad.-75946554523692857652911-i1V___hoNaqWTStRM9101862645149230957031250xdbBC2aBDABEEd73801BEcdD3A7AfecbD864Bdc1862645149230957031259101862645149230957031250xdbBC2aBDABEEd73801BEcdD3A7AfecbD864Bdc186264514923095703125\B___L7D3PS0hd1_Gxkg_Eg2P_Uo_3yd3gU_a_2__Ct28_l1w16r4_p_Q898Fv_22_6_28282357FECfCFCdBEEaE47fb5Bcý{ý1.0xd-0534420.-8911368683772161602973534420.-89113686<-7301351713.7376291319585350ýý [ \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/0ceda9053c751bf974334c7c1676832feea92d69 b/huff0/fuzz/compress/corpus/0ceda9053c751bf974334c7c1676832feea92d69 new file mode 100644 index 0000000000..9e7e94e292 Binary files /dev/null and b/huff0/fuzz/compress/corpus/0ceda9053c751bf974334c7c1676832feea92d69 differ diff --git a/huff0/fuzz/compress/corpus/0dde84d337ceb9269489c3919d205ea9789c158a-5 b/huff0/fuzz/compress/corpus/0dde84d337ceb9269489c3919d205ea9789c158a-5 new file mode 100644 index 0000000000..1382437f3c --- /dev/null +++ b/huff0/fuzz/compress/corpus/0dde84d337ceb9269489c3919d205ea9789c158a-5 @@ -0,0 +1 @@ + ‚Ï01293949596979899; \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/0e90b9893889f69194ca72e28082a1f6081c4b35-1 b/huff0/fuzz/compress/corpus/0e90b9893889f69194ca72e28082a1f6081c4b35-1 new file mode 100644 index 0000000000..0787dae060 --- /dev/null +++ b/huff0/fuzz/compress/corpus/0e90b9893889f69194ca72e28082a1f6081c4b35-1 @@ -0,0 +1 @@ +-08338832110233882812 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/0fcf0d8fa929e950029b365ad631e876ec5f3c47-1 b/huff0/fuzz/compress/corpus/0fcf0d8fa929e950029b365ad631e876ec5f3c47-1 new file mode 100644 index 0000000000..159b67b2a0 Binary files /dev/null and b/huff0/fuzz/compress/corpus/0fcf0d8fa929e950029b365ad631e876ec5f3c47-1 differ diff --git a/huff0/fuzz/compress/corpus/112b9d35e8cbeabce6fda543aec804ab83185543-1 b/huff0/fuzz/compress/corpus/112b9d35e8cbeabce6fda543aec804ab83185543-1 new file mode 100644 index 0000000000..9072d15df0 --- /dev/null +++ b/huff0/fuzz/compress/corpus/112b9d35e8cbeabce6fda543aec804ab83185543-1 @@ -0,0 +1 @@ +8869fecbD864Bdc186264514923095703125-70989128513186264514923095703125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/130efc33e0d8f0327a73110deac85474bc91dc8d-3 b/huff0/fuzz/compress/corpus/130efc33e0d8f0327a73110deac85474bc91dc8d-3 new file mode 100644 index 0000000000..82cbc78af2 Binary files /dev/null and b/huff0/fuzz/compress/corpus/130efc33e0d8f0327a73110deac85474bc91dc8d-3 differ diff --git a/huff0/fuzz/compress/corpus/1eb38d67be98350dd6ce98f72e5b1b75043ce8c7 b/huff0/fuzz/compress/corpus/1eb38d67be98350dd6ce98f72e5b1b75043ce8c7 new file mode 100644 index 0000000000..7a5facee04 --- /dev/null +++ b/huff0/fuzz/compress/corpus/1eb38d67be98350dd6ce98f72e5b1b75043ce8c7 @@ -0,0 +1 @@ +xhy_1P_X_Bers6vz3zZ_95_J_DSW6IkV4_woYf___aC7_p1_1gED_4_Q_28_T73eUL053440.-136868- \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/234ec5ad1fc105fcc08523ff91bd8430284aa1f3 b/huff0/fuzz/compress/corpus/234ec5ad1fc105fcc08523ff91bd8430284aa1f3 new file mode 100644 index 0000000000..684a4432be Binary files /dev/null and b/huff0/fuzz/compress/corpus/234ec5ad1fc105fcc08523ff91bd8430284aa1f3 differ diff --git a/huff0/fuzz/compress/corpus/26ccc59f9c4d89063210f2e8ddd0c26394c2a607-2 b/huff0/fuzz/compress/corpus/26ccc59f9c4d89063210f2e8ddd0c26394c2a607-2 new file mode 100644 index 0000000000..20debe4dd8 --- /dev/null +++ b/huff0/fuzz/compress/corpus/26ccc59f9c4d89063210f2e8ddd0c26394c2a607-2 @@ -0,0 +1 @@ +012756789aefxbcdefx \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/287d18afe5e0d54d4554a39aa2acff99d696136d b/huff0/fuzz/compress/corpus/287d18afe5e0d54d4554a39aa2acff99d696136d new file mode 100644 index 0000000000..299c56cf46 --- /dev/null +++ b/huff0/fuzz/compress/corpus/287d18afe5e0d54d4554a39aa2acff99d696136d @@ -0,0 +1 @@ +8869-0x0e.-9101862645149230957031250xdbBC2aBDABEEd73801BEcdD3A7AfecbD864Bdc186264514923095703125-70989128513186264514923095703125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/29e95908e0f3e9f3e189441de6ed797b7bee2449-2 b/huff0/fuzz/compress/corpus/29e95908e0f3e9f3e189441de6ed797b7bee2449-2 new file mode 100644 index 0000000000..16749a6bf9 --- /dev/null +++ b/huff0/fuzz/compress/corpus/29e95908e0f3e9f3e189441de6ed797b7bee2449-2 @@ -0,0 +1 @@ +0345678defghijklmnopqrstuvwxyz18872161F029739CFeBEbaE7fb-05340.-13686 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/2c93d4381a547aa7001acfffcf51dc323a0cdf2b-10 b/huff0/fuzz/compress/corpus/2c93d4381a547aa7001acfffcf51dc323a0cdf2b-10 new file mode 100644 index 0000000000..02700fd078 Binary files /dev/null and b/huff0/fuzz/compress/corpus/2c93d4381a547aa7001acfffcf51dc323a0cdf2b-10 differ diff --git a/huff0/fuzz/compress/corpus/2e0d10ca05bdebe3fe84c481004adb11a1cc761c-2 b/huff0/fuzz/compress/corpus/2e0d10ca05bdebe3fe84c481004adb11a1cc761c-2 new file mode 100644 index 0000000000..001954fdd7 Binary files /dev/null and b/huff0/fuzz/compress/corpus/2e0d10ca05bdebe3fe84c481004adb11a1cc761c-2 differ diff --git a/huff0/fuzz/compress/corpus/2e85ccbaa127cbf77bb8f0b5c3afdcf53f63f2d8-2 b/huff0/fuzz/compress/corpus/2e85ccbaa127cbf77bb8f0b5c3afdcf53f63f2d8-2 new file mode 100644 index 0000000000..e7c19ad979 --- /dev/null +++ b/huff0/fuzz/compress/corpus/2e85ccbaa127cbf77bb8f0b5c3afdcf53f63f2d8-2 @@ -0,0 +1 @@ +"ï¿4656612873077392578125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/30632e96a06600a75b47b4cbe0db74b959d09aa5 b/huff0/fuzz/compress/corpus/30632e96a06600a75b47b4cbe0db74b959d09aa5 new file mode 100644 index 0000000000..4529b55e3b Binary files /dev/null and b/huff0/fuzz/compress/corpus/30632e96a06600a75b47b4cbe0db74b959d09aa5 differ diff --git a/huff0/fuzz/compress/corpus/308f53f64ce26d56e4379931ff3611a7dd49325f-10 b/huff0/fuzz/compress/corpus/308f53f64ce26d56e4379931ff3611a7dd49325f-10 new file mode 100644 index 0000000000..ec62c5ce57 Binary files /dev/null and b/huff0/fuzz/compress/corpus/308f53f64ce26d56e4379931ff3611a7dd49325f-10 differ diff --git a/huff0/fuzz/compress/corpus/31e020e2f79260ddcbf377c8e1256288b57d32b8-1 b/huff0/fuzz/compress/corpus/31e020e2f79260ddcbf377c8e1256288b57d32b8-1 new file mode 100644 index 0000000000..2e05aff4ae --- /dev/null +++ b/huff0/fuzz/compress/corpus/31e020e2f79260ddcbf377c8e1256288b57d32b8-1 @@ -0,0 +1 @@ +186264514923095703125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/32a3cf4aef238d77278831c3744c1327dcc8f8c0-3 b/huff0/fuzz/compress/corpus/32a3cf4aef238d77278831c3744c1327dcc8f8c0-3 new file mode 100644 index 0000000000..aa7650d5ed Binary files /dev/null and b/huff0/fuzz/compress/corpus/32a3cf4aef238d77278831c3744c1327dcc8f8c0-3 differ diff --git a/huff0/fuzz/compress/corpus/350b061ff78cfb97b28716ec96e27f616d439f97-1 b/huff0/fuzz/compress/corpus/350b061ff78cfb97b28716ec96e27f616d439f97-1 new file mode 100644 index 0000000000..a502d68ede Binary files /dev/null and b/huff0/fuzz/compress/corpus/350b061ff78cfb97b28716ec96e27f616d439f97-1 differ diff --git a/huff0/fuzz/compress/corpus/3542214eb977ea9bdabcc55601d7876b6227b501-2 b/huff0/fuzz/compress/corpus/3542214eb977ea9bdabcc55601d7876b6227b501-2 new file mode 100644 index 0000000000..800763fc7a Binary files /dev/null and b/huff0/fuzz/compress/corpus/3542214eb977ea9bdabcc55601d7876b6227b501-2 differ diff --git a/huff0/fuzz/compress/corpus/3693a58b98ececb7670844f49caa795c1735ddf0-1 b/huff0/fuzz/compress/corpus/3693a58b98ececb7670844f49caa795c1735ddf0-1 new file mode 100644 index 0000000000..51136ddc01 Binary files /dev/null and b/huff0/fuzz/compress/corpus/3693a58b98ececb7670844f49caa795c1735ddf0-1 differ diff --git a/huff0/fuzz/compress/corpus/3b1d88a86a30b301e5f509a756f43ef34fad55b0-2 b/huff0/fuzz/compress/corpus/3b1d88a86a30b301e5f509a756f43ef34fad55b0-2 new file mode 100644 index 0000000000..edf320c82b Binary files /dev/null and b/huff0/fuzz/compress/corpus/3b1d88a86a30b301e5f509a756f43ef34fad55b0-2 differ diff --git a/huff0/fuzz/compress/corpus/3f85a2df398429128e89cb13f51f612645550408-11 b/huff0/fuzz/compress/corpus/3f85a2df398429128e89cb13f51f612645550408-11 new file mode 100644 index 0000000000..7da12c9097 Binary files /dev/null and b/huff0/fuzz/compress/corpus/3f85a2df398429128e89cb13f51f612645550408-11 differ diff --git a/huff0/fuzz/compress/corpus/40b8de701a171fddefdc4459023121a1e605b16c b/huff0/fuzz/compress/corpus/40b8de701a171fddefdc4459023121a1e605b16c new file mode 100644 index 0000000000..ac5355de43 --- /dev/null +++ b/huff0/fuzz/compress/corpus/40b8de701a171fddefdc4459023121a1e605b16c @@ -0,0 +1 @@ +0aaef11119910xC8fFCFCEEaEfb.144-0440.-89116868771616097937988815-9101862645149230957031250xbCaBEBEDAfebDBdc32-xcA3.7475562603-0x0e0862645149230957031250xdbBC2aBDABEEd73801BEcdD3A7AfecbD864Bdc186264514923095703125|7318e000175420 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/4106d42682c6b5f9f068148a3d94f5b52d9a2862-1 b/huff0/fuzz/compress/corpus/4106d42682c6b5f9f068148a3d94f5b52d9a2862-1 new file mode 100644 index 0000000000..bf6dcb3614 Binary files /dev/null and b/huff0/fuzz/compress/corpus/4106d42682c6b5f9f068148a3d94f5b52d9a2862-1 differ diff --git a/huff0/fuzz/compress/corpus/43726b29460c5060782f19cb0facac8443ad5d0b b/huff0/fuzz/compress/corpus/43726b29460c5060782f19cb0facac8443ad5d0b new file mode 100644 index 0000000000..cce03e6dfb --- /dev/null +++ b/huff0/fuzz/compress/corpus/43726b29460c5060782f19cb0facac8443ad5d0b @@ -0,0 +1,2 @@ +*? | " +%`Rw_Y} [QQ.=Rtw ~Pk.Q_k__k__sb_QRif_L_SrsX6i_____UmUzjWyiPsmrgXsPltOrO_____IVSLw_OSN_jnYu_umzY_Rj__v_.ddz,{hyPXrsvzzSWIkVwoY\x&xeýý#y%)ý\ýýICkMYJuZx_KCS_o55_nZ__7LH5_W-1-11252251225127[1162651257125xC2717e61862651257125- VZGohQpFpGtnpGPTniXq_JlK_F__t____GZ__F_Og__VMgb5955baBBBAfbB25929531253._--=x7___Tr438niX_R6q_JlK_mF__t2___4A957_AxmGZ_8_BF_Og_8_VM28cgw491186264514923957031250xbBC2aBDABEEd73801BEcdD3A7AfecbD864Bdc1862645149230957031259101862645149230957031250xdbBC2aBDABEEd73801BEcdD3A7AfecbD864Bdc1862645149230957031253.0xDe15814980304091872B9EEaf48EDccB4b5Da0< \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/43a26357597ae65521480220432ff645450d01f1 b/huff0/fuzz/compress/corpus/43a26357597ae65521480220432ff645450d01f1 new file mode 100644 index 0000000000..2a895564ed Binary files /dev/null and b/huff0/fuzz/compress/corpus/43a26357597ae65521480220432ff645450d01f1 differ diff --git a/huff0/fuzz/compress/corpus/46fc2fe81fed8fdc49861d8a8433560c5a02d503-2 b/huff0/fuzz/compress/corpus/46fc2fe81fed8fdc49861d8a8433560c5a02d503-2 new file mode 100644 index 0000000000..73c64b9ad7 --- /dev/null +++ b/huff0/fuzz/compress/corpus/46fc2fe81fed8fdc49861d8a8433560c5a02d503-2 @@ -0,0 +1 @@ +1136868377216160798828125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/4869e00fbc61867ccab5b83187c3a146ecb36775 b/huff0/fuzz/compress/corpus/4869e00fbc61867ccab5b83187c3a146ecb36775 new file mode 100644 index 0000000000..6897c087cd Binary files /dev/null and b/huff0/fuzz/compress/corpus/4869e00fbc61867ccab5b83187c3a146ecb36775 differ diff --git a/huff0/fuzz/compress/corpus/489d689da4860f3116a62db6b089bb0d5ba4de40 b/huff0/fuzz/compress/corpus/489d689da4860f3116a62db6b089bb0d5ba4de40 new file mode 100644 index 0000000000..3345c8763e Binary files /dev/null and b/huff0/fuzz/compress/corpus/489d689da4860f3116a62db6b089bb0d5ba4de40 differ diff --git a/huff0/fuzz/compress/corpus/48bc76aeb21bc6070323247927d87e591e51eaa9-1 b/huff0/fuzz/compress/corpus/48bc76aeb21bc6070323247927d87e591e51eaa9-1 new file mode 100644 index 0000000000..699dc96c7e Binary files /dev/null and b/huff0/fuzz/compress/corpus/48bc76aeb21bc6070323247927d87e591e51eaa9-1 differ diff --git a/huff0/fuzz/compress/corpus/4959a4a9db83af17f9368cf490b8a0356d872147-1 b/huff0/fuzz/compress/corpus/4959a4a9db83af17f9368cf490b8a0356d872147-1 new file mode 100644 index 0000000000..c139454f98 Binary files /dev/null and b/huff0/fuzz/compress/corpus/4959a4a9db83af17f9368cf490b8a0356d872147-1 differ diff --git a/huff0/fuzz/compress/corpus/49dce428e704656d5a4f1053a443ba44f51e91f0-4 b/huff0/fuzz/compress/corpus/49dce428e704656d5a4f1053a443ba44f51e91f0-4 new file mode 100644 index 0000000000..56577e376e Binary files /dev/null and b/huff0/fuzz/compress/corpus/49dce428e704656d5a4f1053a443ba44f51e91f0-4 differ diff --git a/huff0/fuzz/compress/corpus/4a98d52bab6ca703e4521093afa2a400e10f4a7d-2 b/huff0/fuzz/compress/corpus/4a98d52bab6ca703e4521093afa2a400e10f4a7d-2 new file mode 100644 index 0000000000..7d25588307 Binary files /dev/null and b/huff0/fuzz/compress/corpus/4a98d52bab6ca703e4521093afa2a400e10f4a7d-2 differ diff --git a/huff0/fuzz/compress/corpus/4b4859f278820e969c13c975669322e2c9df9823-5 b/huff0/fuzz/compress/corpus/4b4859f278820e969c13c975669322e2c9df9823-5 new file mode 100644 index 0000000000..74187530eb Binary files /dev/null and b/huff0/fuzz/compress/corpus/4b4859f278820e969c13c975669322e2c9df9823-5 differ diff --git a/huff0/fuzz/compress/corpus/4d1adbc376464b88c52a00ed562cdacbffa4af1a-1 b/huff0/fuzz/compress/corpus/4d1adbc376464b88c52a00ed562cdacbffa4af1a-1 new file mode 100644 index 0000000000..028ba8762d Binary files /dev/null and b/huff0/fuzz/compress/corpus/4d1adbc376464b88c52a00ed562cdacbffa4af1a-1 differ diff --git a/huff0/fuzz/compress/corpus/4f506d462241876610a0f92a5aa1e2b38ea8722c-7 b/huff0/fuzz/compress/corpus/4f506d462241876610a0f92a5aa1e2b38ea8722c-7 new file mode 100644 index 0000000000..882d595fb3 Binary files /dev/null and b/huff0/fuzz/compress/corpus/4f506d462241876610a0f92a5aa1e2b38ea8722c-7 differ diff --git a/huff0/fuzz/compress/corpus/51bdcd2cbd098b042fddc528083572bdbcfbe449-1 b/huff0/fuzz/compress/corpus/51bdcd2cbd098b042fddc528083572bdbcfbe449-1 new file mode 100644 index 0000000000..a73933e681 Binary files /dev/null and b/huff0/fuzz/compress/corpus/51bdcd2cbd098b042fddc528083572bdbcfbe449-1 differ diff --git a/huff0/fuzz/compress/corpus/51c91bfe65eaa40fe4144306d6332ad84e4bb8ed-1 b/huff0/fuzz/compress/corpus/51c91bfe65eaa40fe4144306d6332ad84e4bb8ed-1 new file mode 100644 index 0000000000..12301f1709 --- /dev/null +++ b/huff0/fuzz/compress/corpus/51c91bfe65eaa40fe4144306d6332ad84e4bb8ed-1 @@ -0,0 +1 @@ +GPTrniXRqJlK_mFt____AmGZ__BF_Og__VM_gw.4995325xC2aBABd731BEd3A7Aecb8dc866414295731253.0xDe9D94B9EEaf48EDccB4b5Da0-0x0e-9101862645149230957031250xdbBC2aBDABEEd73801BEcdD3A7AfecbD864Bdc186264514923095703125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/53bcc0e78275d3efe9e012e6d30412066bf354f9-2 b/huff0/fuzz/compress/corpus/53bcc0e78275d3efe9e012e6d30412066bf354f9-2 new file mode 100644 index 0000000000..e59672dfc2 --- /dev/null +++ b/huff0/fuzz/compress/corpus/53bcc0e78275d3efe9e012e6d30412066bf354f9-2 @@ -0,0 +1 @@ +4995325233449559994-09-519341245142309503125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/545828581071b0d01c3fd9d15c7850c3f83ea15f-2 b/huff0/fuzz/compress/corpus/545828581071b0d01c3fd9d15c7850c3f83ea15f-2 new file mode 100644 index 0000000000..b8bf81e791 Binary files /dev/null and b/huff0/fuzz/compress/corpus/545828581071b0d01c3fd9d15c7850c3f83ea15f-2 differ diff --git a/huff0/fuzz/compress/corpus/56860900c11ef30e85ce99b14e0e687745b269ae-9 b/huff0/fuzz/compress/corpus/56860900c11ef30e85ce99b14e0e687745b269ae-9 new file mode 100644 index 0000000000..794b9115f3 Binary files /dev/null and b/huff0/fuzz/compress/corpus/56860900c11ef30e85ce99b14e0e687745b269ae-9 differ diff --git a/huff0/fuzz/compress/corpus/581ed2fb7f25a6a5a8de40beae3dfd1666c511d0-2 b/huff0/fuzz/compress/corpus/581ed2fb7f25a6a5a8de40beae3dfd1666c511d0-2 new file mode 100644 index 0000000000..a67655c657 --- /dev/null +++ b/huff0/fuzz/compress/corpus/581ed2fb7f25a6a5a8de40beae3dfd1666c511d0-2 @@ -0,0 +1 @@ +92309570312 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/5b5d4262260375deb5146e3fa43f689923e856a7 b/huff0/fuzz/compress/corpus/5b5d4262260375deb5146e3fa43f689923e856a7 new file mode 100644 index 0000000000..3340915b60 Binary files /dev/null and b/huff0/fuzz/compress/corpus/5b5d4262260375deb5146e3fa43f689923e856a7 differ diff --git a/huff0/fuzz/compress/corpus/5c4b6679ee378f2533b099450ed21e40d54f6cd0-3 b/huff0/fuzz/compress/corpus/5c4b6679ee378f2533b099450ed21e40d54f6cd0-3 new file mode 100644 index 0000000000..e2127e696c --- /dev/null +++ b/huff0/fuzz/compress/corpus/5c4b6679ee378f2533b099450ed21e40d54f6cd0-3 @@ -0,0 +1 @@ +"ï¿465663981283077397778125 \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/5e13404b6713db5f2d4972747da143057beaaf64-2 b/huff0/fuzz/compress/corpus/5e13404b6713db5f2d4972747da143057beaaf64-2 new file mode 100644 index 0000000000..2189bb453e --- /dev/null +++ b/huff0/fuzz/compress/corpus/5e13404b6713db5f2d4972747da143057beaaf64-2 @@ -0,0 +1 @@ +012@456789@4567x \ No newline at end of file diff --git a/huff0/fuzz/compress/corpus/5e6c62abd0fe49668d26e9bfced0ef0dc699f6a5-3 b/huff0/fuzz/compress/corpus/5e6c62abd0fe49668d26e9bfced0ef0dc699f6a5-3 new file mode 100644 index 0000000000..90d6f0afc9 Binary files /dev/null and b/huff0/fuzz/compress/corpus/5e6c62abd0fe49668d26e9bfced0ef0dc699f6a5-3 differ diff --git a/huff0/fuzz/compress/corpus/5e701f4039d099c3a15c4e8d9b070eafc96de0e9 b/huff0/fuzz/compress/corpus/5e701f4039d099c3a15c4e8d9b070eafc96de0e9 new file mode 100644 index 0000000000..ff8e00b9b1 --- /dev/null +++ b/huff0/fuzz/compress/corpus/5e701f4039d099c3a15c4e8d9b070eafc96de0e9 @@ -0,0 +1 @@ + 0003> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + return uint32(deBruijnClz[(v*0x07C4ACDD)>>27]) +} diff --git a/huff0/huff0.go b/huff0/huff0.go new file mode 100644 index 0000000000..2f2df1688b --- /dev/null +++ b/huff0/huff0.go @@ -0,0 +1,238 @@ +package huff0 + +import ( + "errors" + "fmt" + "math" + + "github.com/klauspost/compress/fse" +) + +const ( + maxSymbolValue = 255 + + // zstandard limits tablelog to 11, see: + // https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#huffman-tree-description + tableLogMax = 11 + tableLogDefault = 11 + minTablelog = 5 + huffNodesLen = 512 + + // BlockSizeMax is maximum input size for a single block uncompressed. + BlockSizeMax = 128 << 10 +) + +var ( + // ErrIncompressible is returned when input is judged to be too hard to compress. + ErrIncompressible = errors.New("input is not compressible") + + // ErrUseRLE is returned from the compressor when the input is a single byte value repeated. + ErrUseRLE = errors.New("input is single value repeated") + + // ErrTooBig is return if input is too large for a single block. + ErrTooBig = errors.New("input too big") +) + +type ReusePolicy uint8 + +const ( + // ReusePolicyAllow will allow reuse if it produces smaller output. + ReusePolicyAllow ReusePolicy = iota + + // ReusePolicyPrefer will re-use aggressively if possible. + // This will not check if a new table will produce smaller output, + // except if the current table is impossible to use or + // compressed output is bigger than input. + ReusePolicyPrefer + + // ReusePolicyNone will disable re-use of tables. + // This is slightly faster than ReusePolicyAllow but may produce larger output. + ReusePolicyNone +) + +type Scratch struct { + count [maxSymbolValue + 1]uint32 + + // Per block parameters. + // These can be used to override compression parameters of the block. + // Do not touch, unless you know what you are doing. + + // Out is output buffer. + // If the scratch is re-used before the caller is done processing the output, + // set this field to nil. + // Otherwise the output buffer will be re-used for next Compression/Decompression step + // and allocation will be avoided. + Out []byte + + // OutTable will contain the table data only, if a new table has been generated. + // Slice of the returned data. + OutTable []byte + + // OutData will contain the compressed data. + // Slice of the returned data. + OutData []byte + + // MaxSymbolValue will override the maximum symbol value of the next block. + MaxSymbolValue uint8 + + // TableLog will attempt to override the tablelog for the next block. + // Must be <= 11. + TableLog uint8 + + // Reuse will specify the reuse policy + Reuse ReusePolicy + + br byteReader + symbolLen uint16 // Length of active part of the symbol table. + maxCount int // count of the most probable symbol + clearCount bool // clear count + actualTableLog uint8 // Selected tablelog. + prevTable cTable // Table used for previous compression. + cTable cTable // compression table + dt dTable // decompression table + nodes []nodeElt + tmpOut [4][]byte + fse *fse.Scratch + huffWeight [maxSymbolValue + 1]byte +} + +func (s *Scratch) prepare(in []byte) (*Scratch, error) { + if len(in) > BlockSizeMax { + return nil, ErrTooBig + } + if s == nil { + s = &Scratch{} + } + if s.MaxSymbolValue == 0 { + s.MaxSymbolValue = maxSymbolValue + } + if s.TableLog == 0 { + s.TableLog = tableLogDefault + } + if s.TableLog > tableLogMax { + return nil, fmt.Errorf("tableLog (%d) > maxTableLog (%d)", s.TableLog, tableLogMax) + } + if s.clearCount && s.maxCount == 0 { + for i := range s.count { + s.count[i] = 0 + } + s.clearCount = false + } + if cap(s.Out) == 0 { + s.Out = make([]byte, 0, len(in)) + } + s.Out = s.Out[:0] + if cap(s.cTable) < maxSymbolValue+1 { + s.cTable = make([]cTableEntry, 0, maxSymbolValue+1) + } + s.cTable = s.cTable[:0] + if cap(s.prevTable) < maxSymbolValue+1 { + s.prevTable = make([]cTableEntry, 0, maxSymbolValue+1) + } + s.OutTable = nil + s.OutData = nil + if cap(s.nodes) < huffNodesLen+1 { + s.nodes = make([]nodeElt, 0, huffNodesLen+1) + } + if s.fse == nil { + s.fse = &fse.Scratch{} + } + s.nodes = s.nodes[:0] + s.br.init(in) + + return s, nil +} + +type cTable []cTableEntry + +func (c cTable) write(s *Scratch) error { + var ( + // precomputed conversion table + bitsToWeight [tableLogMax + 1]byte + huffLog = s.actualTableLog + // last weight is not saved. + maxSymbolValue = uint8(s.symbolLen - 1) + huffWeight = s.huffWeight[:256] + ) + const ( + maxFSETableLog = 6 + ) + // convert to weight + bitsToWeight[0] = 0 + for n := uint8(1); n < huffLog+1; n++ { + bitsToWeight[n] = huffLog + 1 - n + } + + // Acquire histogram for FSE. + hist := s.fse.Histogram() + hist = hist[:256] + for i := range hist[:16] { + hist[i] = 0 + } + for n := uint8(0); n < maxSymbolValue; n++ { + v := bitsToWeight[c[n].nBits] & 15 + huffWeight[n] = v + hist[v]++ + } + + // FSE compress if feasible. + if maxSymbolValue >= 2 { + huffMaxCnt := uint32(0) + huffMax := uint8(0) + for i, v := range hist[:16] { + if v == 0 { + continue + } + huffMax = byte(i) + if v > huffMaxCnt { + huffMaxCnt = v + } + } + s.fse.HistogramFinished(huffMax, int(huffMaxCnt)) + s.fse.TableLog = maxFSETableLog + b, err := fse.Compress(huffWeight[:maxSymbolValue], s.fse) + if err == nil && len(b) < int(s.symbolLen>>1) { + s.Out = append(s.Out, uint8(len(b))) + s.Out = append(s.Out, b...) + return nil + } + } + // write raw values as 4-bits (max : 15) + if maxSymbolValue > (256 - 128) { + // should not happen : likely means source cannot be compressed + return ErrIncompressible + } + op := s.Out + // special case, pack weights 4 bits/weight. + op = append(op, 128|(maxSymbolValue-1)) + // be sure it doesn't cause msan issue in final combination + huffWeight[maxSymbolValue] = 0 + for n := uint16(0); n < uint16(maxSymbolValue); n += 2 { + op = append(op, (huffWeight[n]<<4)|huffWeight[n+1]) + } + s.Out = op + return nil +} + +// estimateSize returns the estimated size in bytes of the input represented in the +// histogram supplied. +func (c cTable) estimateSize(hist []uint32) int { + nbBits := uint32(7) + for i, v := range c[:len(hist)] { + nbBits += uint32(v.nBits) * hist[i] + } + return int(nbBits >> 3) +} + +// minSize returns the minimum possible size considering the shannon limit. +func (s *Scratch) minSize(total int) int { + nbBits := float64(7) + fTotal := float64(total) + for _, v := range s.count[:s.symbolLen] { + n := float64(v) + if n > 0 { + nbBits += math.Log2(fTotal/n) * n + } + } + return int(nbBits) >> 3 +}