Skip to content

Commit b7ccab8

Browse files
authored
Add stateless dictionary support (#216)
* Add stateless dictionary support Enable up to 8K dictionary for stateless compression.
1 parent 4f93024 commit b7ccab8

24 files changed

+87
-24
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ This package provides various compression algorithms.
1515

1616
# changelog
1717

18+
* Feb 4, 2020: (v1.10.0) Add optional dictionary to [stateless deflate](https://pkg.go.dev/github.com/klauspost/compress/flate?tab=doc#StatelessDeflate). Breaking change, send `nil` for previous behaviour. [#216](https://github.com/klauspost/compress/pull/216)
19+
* Feb 3, 2020: Fix buffer overflow on repeated small block deflate. [#218](https://github.com/klauspost/compress/pull/218)
1820
* Jan 31, 2020: Allow copying content from an existing ZIP file without decompressing+compressing. [#214](https://github.com/klauspost/compress/pull/214)
1921
* Jan 28, 2020: Added [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) AMD64 assembler and various optimizations. Stream speed >10GB/s. [#186](https://github.com/klauspost/compress/pull/186)
2022
* Jan 20,2020 (v1.9.8) Optimize gzip/deflate with better size estimates and faster table generation. [#207](https://github.com/klauspost/compress/pull/207) by [luyu6056](https://github.com/luyu6056), [#206](https://github.com/klauspost/compress/pull/206).

flate/flate_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,31 @@ func TestRegressions(t *testing.T) {
138138
}
139139
})
140140
}
141+
t.Run(tt.Name+"stateless", func(t *testing.T) {
142+
// Split into two and use history...
143+
buf := new(bytes.Buffer)
144+
err = StatelessDeflate(buf, data1[:len(data1)/2], false, nil)
145+
if err != nil {
146+
t.Error(err)
147+
}
148+
149+
// Use top half as dictionary...
150+
dict := data1[:len(data1)/2]
151+
err = StatelessDeflate(buf, data1[len(data1)/2:], true, dict)
152+
if err != nil {
153+
t.Error(err)
154+
}
155+
t.Log(buf.Len())
156+
fr1 := NewReader(buf)
157+
data2, err := ioutil.ReadAll(fr1)
158+
if err != nil {
159+
t.Error(err)
160+
}
161+
if bytes.Compare(data1, data2) != 0 {
162+
fmt.Printf("want:%x\ngot: %x\n", data1, data2)
163+
t.Error("not equal")
164+
}
165+
})
141166
}
142167
}
143168

flate/huffman_bit_writer.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ func (w *huffmanBitWriter) flush() {
177177
w.nbits = 0
178178
return
179179
}
180+
if w.lastHeader > 0 {
181+
// We owe an EOB
182+
w.writeCode(w.literalEncoding.codes[endBlockMarker])
183+
w.lastHeader = 0
184+
}
180185
n := w.nbytes
181186
for w.nbits != 0 {
182187
w.bytes[n] = byte(w.bits)
@@ -594,8 +599,8 @@ func (w *huffmanBitWriter) writeBlockDynamic(tokens *tokens, eof bool, input []b
594599
tokens.AddEOB()
595600
}
596601

597-
// We cannot reuse pure huffman table.
598-
if w.lastHuffMan && w.lastHeader > 0 {
602+
// We cannot reuse pure huffman table, and must mark as EOF.
603+
if (w.lastHuffMan || eof) && w.lastHeader > 0 {
599604
// We will not try to reuse.
600605
w.writeCode(w.literalEncoding.codes[endBlockMarker])
601606
w.lastHeader = 0

flate/stateless.go

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88

99
const (
1010
maxStatelessBlock = math.MaxInt16
11+
// dictionary will be taken from maxStatelessBlock, so limit it.
12+
maxStatelessDict = 8 << 10
1113

1214
slTableBits = 13
1315
slTableSize = 1 << slTableBits
@@ -25,11 +27,11 @@ func (s *statelessWriter) Close() error {
2527
}
2628
s.closed = true
2729
// Emit EOF block
28-
return StatelessDeflate(s.dst, nil, true)
30+
return StatelessDeflate(s.dst, nil, true, nil)
2931
}
3032

3133
func (s *statelessWriter) Write(p []byte) (n int, err error) {
32-
err = StatelessDeflate(s.dst, p, false)
34+
err = StatelessDeflate(s.dst, p, false, nil)
3335
if err != nil {
3436
return 0, err
3537
}
@@ -59,7 +61,10 @@ var bitWriterPool = sync.Pool{
5961

6062
// StatelessDeflate allows to compress directly to a Writer without retaining state.
6163
// When returning everything will be flushed.
62-
func StatelessDeflate(out io.Writer, in []byte, eof bool) error {
64+
// Up to 8KB of an optional dictionary can be given which is presumed to presumed to precede the block.
65+
// Longer dictionaries will be truncated and will still produce valid output.
66+
// Sending nil dictionary is perfectly fine.
67+
func StatelessDeflate(out io.Writer, in []byte, eof bool, dict []byte) error {
6368
var dst tokens
6469
bw := bitWriterPool.Get().(*huffmanBitWriter)
6570
bw.reset(out)
@@ -76,35 +81,53 @@ func StatelessDeflate(out io.Writer, in []byte, eof bool) error {
7681
return bw.err
7782
}
7883

84+
// Truncate dict
85+
if len(dict) > maxStatelessDict {
86+
dict = dict[len(dict)-maxStatelessDict:]
87+
}
88+
7989
for len(in) > 0 {
8090
todo := in
81-
if len(todo) > maxStatelessBlock {
82-
todo = todo[:maxStatelessBlock]
91+
if len(todo) > maxStatelessBlock-len(dict) {
92+
todo = todo[:maxStatelessBlock-len(dict)]
8393
}
8494
in = in[len(todo):]
95+
uncompressed := todo
96+
if len(dict) > 0 {
97+
// combine dict and source
98+
bufLen := len(todo) + len(dict)
99+
combined := make([]byte, bufLen)
100+
copy(combined, dict)
101+
copy(combined[len(dict):], todo)
102+
todo = combined
103+
}
85104
// Compress
86-
statelessEnc(&dst, todo)
105+
statelessEnc(&dst, todo, int16(len(dict)))
87106
isEof := eof && len(in) == 0
88107

89108
if dst.n == 0 {
90-
bw.writeStoredHeader(len(todo), isEof)
109+
bw.writeStoredHeader(len(uncompressed), isEof)
91110
if bw.err != nil {
92111
return bw.err
93112
}
94-
bw.writeBytes(todo)
95-
} else if int(dst.n) > len(todo)-len(todo)>>4 {
113+
bw.writeBytes(uncompressed)
114+
} else if int(dst.n) > len(uncompressed)-len(uncompressed)>>4 {
96115
// If we removed less than 1/16th, huffman compress the block.
97-
bw.writeBlockHuff(isEof, todo, false)
116+
bw.writeBlockHuff(isEof, uncompressed, len(in) == 0)
98117
} else {
99-
bw.writeBlockDynamic(&dst, isEof, todo, false)
118+
bw.writeBlockDynamic(&dst, isEof, uncompressed, len(in) == 0)
119+
}
120+
if len(in) > 0 {
121+
// Retain a dict if we have more
122+
dict = todo[len(todo)-maxStatelessDict:]
123+
dst.Reset()
100124
}
101125
if bw.err != nil {
102126
return bw.err
103127
}
104-
dst.Reset()
105128
}
106129
if !eof {
107-
// Align.
130+
// Align, only a stored block can do that.
108131
bw.writeStoredHeader(0, false)
109132
}
110133
bw.flush()
@@ -130,7 +153,7 @@ func load6416(b []byte, i int16) uint64 {
130153
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
131154
}
132155

133-
func statelessEnc(dst *tokens, src []byte) {
156+
func statelessEnc(dst *tokens, src []byte, startAt int16) {
134157
const (
135158
inputMargin = 12 - 1
136159
minNonLiteralBlockSize = 1 + 1 + inputMargin
@@ -144,15 +167,23 @@ func statelessEnc(dst *tokens, src []byte) {
144167

145168
// This check isn't in the Snappy implementation, but there, the caller
146169
// instead of the callee handles this case.
147-
if len(src) < minNonLiteralBlockSize {
170+
if len(src)-int(startAt) < minNonLiteralBlockSize {
148171
// We do not fill the token table.
149172
// This will be picked up by caller.
150-
dst.n = uint16(len(src))
173+
dst.n = 0
151174
return
152175
}
176+
// Index until startAt
177+
if startAt > 0 {
178+
cv := load3232(src, 0)
179+
for i := int16(0); i < startAt; i++ {
180+
table[hashSL(cv)] = tableEntry{offset: i}
181+
cv = (cv >> 8) | (uint32(src[i+4]) << 24)
182+
}
183+
}
153184

154-
s := int16(1)
155-
nextEmit := int16(0)
185+
s := startAt + 1
186+
nextEmit := startAt
156187
// sLimit is when to stop looking for offset/length copies. The inputMargin
157188
// lets us use a fast path for emitLiteral in the main loop, while we are
158189
// looking for copies.
1 Byte
Binary file not shown.
Binary file not shown.
1 Byte
Binary file not shown.

flate/testdata/huffman-pi.dyn.expect

1 Byte
Binary file not shown.
1 Byte
Binary file not shown.

flate/testdata/huffman-pi.golden

1 Byte
Binary file not shown.

0 commit comments

Comments
 (0)