Skip to content

Commit 5c66724

Browse files
eustascopybara-github
authored andcommitted
Add dictionary API to cgo wrapper
PiperOrigin-RevId: 698745795
1 parent 39904bd commit 5c66724

File tree

3 files changed

+160
-19
lines changed

3 files changed

+160
-19
lines changed

go/cbrotli/cbrotli_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,44 @@ func TestEncodeDecode(t *testing.T) {
375375
}
376376
}
377377
}
378+
379+
func TestEncodeDecodeWithDictionary(t *testing.T) {
380+
q := 5
381+
l := 4096
382+
383+
input := make([]byte, l)
384+
for i := 0; i < l; i++ {
385+
input[i] = byte(i*7 + i*i*5)
386+
}
387+
// use dictionary same as input
388+
pd := NewPreparedDictionary(input, DtRaw, q)
389+
defer pd.Close()
390+
391+
encoded, err := Encode(input, WriterOptions{Quality: q, Dictionary: pd})
392+
if err != nil {
393+
t.Errorf("Encode: %v", err)
394+
}
395+
limit := 20
396+
if len(encoded) > limit {
397+
t.Errorf("Output length exceeds expectations: %d > %d", len(encoded), limit)
398+
}
399+
400+
decoded, err := DecodeWithRawDictionary(encoded, input)
401+
if err != nil {
402+
t.Errorf("Decode: %v", err)
403+
}
404+
if !bytes.Equal(decoded, input) {
405+
var want string
406+
if len(input) > 320 {
407+
want = fmt.Sprintf("<%d bytes>", len(input))
408+
} else {
409+
want = fmt.Sprintf("%q", input)
410+
}
411+
t.Errorf(""+
412+
"Decode content:\n"+
413+
"%q\n"+
414+
"want:\n"+
415+
"%s",
416+
decoded, want)
417+
}
418+
}

go/cbrotli/reader.go

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"errors"
3434
"io"
3535
"io/ioutil"
36+
"runtime"
3637
)
3738

3839
type decodeError C.BrotliDecoderErrorCode
@@ -49,10 +50,11 @@ var errReaderClosed = errors.New("cbrotli: Reader is closed")
4950
// Reader implements io.ReadCloser by reading Brotli-encoded data from an
5051
// underlying Reader.
5152
type Reader struct {
52-
src io.Reader
53-
state *C.BrotliDecoderState
54-
buf []byte // scratch space for reading from src
55-
in []byte // current chunk to decode; usually aliases buf
53+
src io.Reader
54+
state *C.BrotliDecoderState
55+
buf []byte // scratch space for reading from src
56+
in []byte // current chunk to decode; usually aliases buf
57+
pinner *runtime.Pinner // raw dictionary pinner
5658
}
5759

5860
// readBufSize is a "good" buffer size that avoids excessive round-trips
@@ -63,10 +65,26 @@ const readBufSize = 32 * 1024
6365
// NewReader initializes new Reader instance.
6466
// Close MUST be called to free resources.
6567
func NewReader(src io.Reader) *Reader {
68+
return NewReaderWithRawDictionary(src, nil)
69+
}
70+
71+
// NewReaderWithRawDictionary initializes new Reader instance with shared dictionary.
72+
// Close MUST be called to free resources.
73+
func NewReaderWithRawDictionary(src io.Reader, dictionary []byte) *Reader {
74+
s := C.BrotliDecoderCreateInstance(nil, nil, nil)
75+
var p *runtime.Pinner
76+
if dictionary != nil {
77+
p = new(runtime.Pinner)
78+
p.Pin(&dictionary[0])
79+
// TODO(eustas): use return value
80+
C.BrotliDecoderAttachDictionary(s, C.BrotliSharedDictionaryType( /* RAW */ 0),
81+
C.size_t(len(dictionary)), (*C.uint8_t)(&dictionary[0]))
82+
}
6683
return &Reader{
67-
src: src,
68-
state: C.BrotliDecoderCreateInstance(nil, nil, nil),
69-
buf: make([]byte, readBufSize),
84+
src: src,
85+
state: s,
86+
buf: make([]byte, readBufSize),
87+
pinner: p,
7088
}
7189
}
7290

@@ -78,6 +96,10 @@ func (r *Reader) Close() error {
7896
// Close despite the state; i.e. there might be some unread decoded data.
7997
C.BrotliDecoderDestroyInstance(r.state)
8098
r.state = nil
99+
if r.pinner != nil {
100+
r.pinner.Unpin()
101+
r.pinner = nil
102+
}
81103
return nil
82104
}
83105

@@ -153,11 +175,26 @@ func (r *Reader) Read(p []byte) (n int, err error) {
153175

154176
// Decode decodes Brotli encoded data.
155177
func Decode(encodedData []byte) ([]byte, error) {
178+
return DecodeWithRawDictionary(encodedData, nil)
179+
}
180+
181+
// DecodeWithRawDictionary decodes Brotli encoded data with shared dictionary.
182+
func DecodeWithRawDictionary(encodedData []byte, dictionary []byte) ([]byte, error) {
183+
s := C.BrotliDecoderCreateInstance(nil, nil, nil)
184+
var p *runtime.Pinner
185+
if dictionary != nil {
186+
p = new(runtime.Pinner)
187+
p.Pin(&dictionary[0])
188+
// TODO(eustas): use return value
189+
C.BrotliDecoderAttachDictionary(s, C.BrotliSharedDictionaryType( /* RAW */ 0),
190+
C.size_t(len(dictionary)), (*C.uint8_t)(&dictionary[0]))
191+
}
156192
r := &Reader{
157-
src: bytes.NewReader(nil),
158-
state: C.BrotliDecoderCreateInstance(nil, nil, nil),
159-
buf: make([]byte, 4), // arbitrarily small but nonzero so that r.src.Read returns io.EOF
160-
in: encodedData,
193+
src: bytes.NewReader(nil),
194+
state: s,
195+
buf: make([]byte, 4), // arbitrarily small but nonzero so that r.src.Read returns io.EOF
196+
in: encodedData,
197+
pinner: p,
161198
}
162199
defer r.Close()
163200
return ioutil.ReadAll(r)

go/cbrotli/writer.go

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,54 @@ import (
4545
"bytes"
4646
"errors"
4747
"io"
48+
"runtime"
4849
"unsafe"
4950
)
5051

52+
// PreparedDictionary is a handle to native object.
53+
type PreparedDictionary struct {
54+
opaque *C.BrotliEncoderPreparedDictionary
55+
pinner *runtime.Pinner
56+
}
57+
58+
// DictionaryType is type for shared dictionary
59+
type DictionaryType int
60+
61+
const (
62+
// DtRaw denotes LZ77 prefix dictionary
63+
DtRaw DictionaryType = 0
64+
// DtSerialized denotes serialized format
65+
DtSerialized DictionaryType = 1
66+
)
67+
68+
// NewPreparedDictionary prepares dictionary data for encoder.
69+
// Same instance can be used for multiple encoding sessions.
70+
// Close MUST be called to free resources.
71+
func NewPreparedDictionary(data []byte, dictionaryType DictionaryType, quality int) *PreparedDictionary {
72+
var ptr *C.uint8_t
73+
if len(data) != 0 {
74+
ptr = (*C.uint8_t)(&data[0])
75+
}
76+
p := new(runtime.Pinner)
77+
p.Pin(&data[0])
78+
d := C.BrotliEncoderPrepareDictionary(C.BrotliSharedDictionaryType(dictionaryType), C.size_t(len(data)), ptr, C.int(quality), nil, nil, nil)
79+
return &PreparedDictionary{
80+
opaque: d,
81+
pinner: p,
82+
}
83+
}
84+
85+
// Close frees C resources.
86+
// IMPORTANT: calling Close until all encoders that use that dictionary are closed as well will
87+
// cause crash.
88+
func (p *PreparedDictionary) Close() error {
89+
// C-Brotli tolerates `nil` pointer here.
90+
C.BrotliEncoderDestroyPreparedDictionary(p.opaque)
91+
p.opaque = nil
92+
p.pinner.Unpin()
93+
return nil
94+
}
95+
5196
// WriterOptions configures Writer.
5297
type WriterOptions struct {
5398
// Quality controls the compression-speed vs compression-density trade-offs.
@@ -56,38 +101,56 @@ type WriterOptions struct {
56101
// LGWin is the base 2 logarithm of the sliding window size.
57102
// Range is 10 to 24. 0 indicates automatic configuration based on Quality.
58103
LGWin int
104+
// Prepared shared dictionary
105+
Dictionary *PreparedDictionary
59106
}
60107

61108
// Writer implements io.WriteCloser by writing Brotli-encoded data to an
62109
// underlying Writer.
63110
type Writer struct {
111+
healthy bool
64112
dst io.Writer
65113
state *C.BrotliEncoderState
66114
buf, encoded []byte
67115
}
68116

69117
var (
70-
errEncode = errors.New("cbrotli: encode error")
71-
errWriterClosed = errors.New("cbrotli: Writer is closed")
118+
errEncode = errors.New("cbrotli: encode error")
119+
errWriterClosed = errors.New("cbrotli: Writer is closed")
120+
errWriterUnhealthy = errors.New("cbrotli: Writer is unhealthy")
72121
)
73122

74123
// NewWriter initializes new Writer instance.
75124
// Close MUST be called to free resources.
76125
func NewWriter(dst io.Writer, options WriterOptions) *Writer {
77126
state := C.BrotliEncoderCreateInstance(nil, nil, nil)
78-
C.BrotliEncoderSetParameter(
79-
state, C.BROTLI_PARAM_QUALITY, (C.uint32_t)(options.Quality))
127+
healthy := state != nil
128+
if C.BrotliEncoderSetParameter(
129+
state, C.BROTLI_PARAM_QUALITY, (C.uint32_t)(options.Quality)) == 0 {
130+
healthy = false
131+
}
80132
if options.LGWin > 0 {
81-
C.BrotliEncoderSetParameter(
82-
state, C.BROTLI_PARAM_LGWIN, (C.uint32_t)(options.LGWin))
133+
if C.BrotliEncoderSetParameter(
134+
state, C.BROTLI_PARAM_LGWIN, (C.uint32_t)(options.LGWin)) == 0 {
135+
healthy = false
136+
}
137+
}
138+
if options.Dictionary != nil {
139+
if C.BrotliEncoderAttachPreparedDictionary(state, options.Dictionary.opaque) == 0 {
140+
healthy = false
141+
}
83142
}
84143
return &Writer{
85-
dst: dst,
86-
state: state,
144+
healthy: healthy,
145+
dst: dst,
146+
state: state,
87147
}
88148
}
89149

90150
func (w *Writer) writeChunk(p []byte, op C.BrotliEncoderOperation) (n int, err error) {
151+
if !w.healthy {
152+
return 0, errWriterUnhealthy
153+
}
91154
if w.state == nil {
92155
return 0, errWriterClosed
93156
}

0 commit comments

Comments
 (0)