Add dictionary API to cgo wrapper
PiperOrigin-RevId: 698745795
diff --git a/go/cbrotli/cbrotli_test.go b/go/cbrotli/cbrotli_test.go
index 254c0cd..5d15eaa 100644
--- a/go/cbrotli/cbrotli_test.go
+++ b/go/cbrotli/cbrotli_test.go
@@ -375,3 +375,44 @@
}
}
}
+
+func TestEncodeDecodeWithDictionary(t *testing.T) {
+ q := 5
+ l := 4096
+
+ input := make([]byte, l)
+ for i := 0; i < l; i++ {
+ input[i] = byte(i*7 + i*i*5)
+ }
+ // use dictionary same as input
+ pd := NewPreparedDictionary(input, DtRaw, q)
+ defer pd.Close()
+
+ encoded, err := Encode(input, WriterOptions{Quality: q, Dictionary: pd})
+ if err != nil {
+ t.Errorf("Encode: %v", err)
+ }
+ limit := 20
+ if len(encoded) > limit {
+ t.Errorf("Output length exceeds expectations: %d > %d", len(encoded), limit)
+ }
+
+ decoded, err := DecodeWithRawDictionary(encoded, input)
+ if err != nil {
+ t.Errorf("Decode: %v", err)
+ }
+ if !bytes.Equal(decoded, input) {
+ var want string
+ if len(input) > 320 {
+ want = fmt.Sprintf("<%d bytes>", len(input))
+ } else {
+ want = fmt.Sprintf("%q", input)
+ }
+ t.Errorf(""+
+ "Decode content:\n"+
+ "%q\n"+
+ "want:\n"+
+ "%s",
+ decoded, want)
+ }
+}
diff --git a/go/cbrotli/reader.go b/go/cbrotli/reader.go
index 6e390c3..6a6aa49 100644
--- a/go/cbrotli/reader.go
+++ b/go/cbrotli/reader.go
@@ -33,6 +33,7 @@
"errors"
"io"
"io/ioutil"
+ "runtime"
)
type decodeError C.BrotliDecoderErrorCode
@@ -49,10 +50,11 @@
// Reader implements io.ReadCloser by reading Brotli-encoded data from an
// underlying Reader.
type Reader struct {
- src io.Reader
- state *C.BrotliDecoderState
- buf []byte // scratch space for reading from src
- in []byte // current chunk to decode; usually aliases buf
+ src io.Reader
+ state *C.BrotliDecoderState
+ buf []byte // scratch space for reading from src
+ in []byte // current chunk to decode; usually aliases buf
+ pinner *runtime.Pinner // raw dictionary pinner
}
// readBufSize is a "good" buffer size that avoids excessive round-trips
@@ -63,10 +65,26 @@
// NewReader initializes new Reader instance.
// Close MUST be called to free resources.
func NewReader(src io.Reader) *Reader {
+ return NewReaderWithRawDictionary(src, nil)
+}
+
+// NewReaderWithRawDictionary initializes new Reader instance with shared dictionary.
+// Close MUST be called to free resources.
+func NewReaderWithRawDictionary(src io.Reader, dictionary []byte) *Reader {
+ s := C.BrotliDecoderCreateInstance(nil, nil, nil)
+ var p *runtime.Pinner
+ if dictionary != nil {
+ p = new(runtime.Pinner)
+ p.Pin(&dictionary[0])
+ // TODO(eustas): use return value
+ C.BrotliDecoderAttachDictionary(s, C.BrotliSharedDictionaryType( /* RAW */ 0),
+ C.size_t(len(dictionary)), (*C.uint8_t)(&dictionary[0]))
+ }
return &Reader{
- src: src,
- state: C.BrotliDecoderCreateInstance(nil, nil, nil),
- buf: make([]byte, readBufSize),
+ src: src,
+ state: s,
+ buf: make([]byte, readBufSize),
+ pinner: p,
}
}
@@ -78,6 +96,10 @@
// Close despite the state; i.e. there might be some unread decoded data.
C.BrotliDecoderDestroyInstance(r.state)
r.state = nil
+ if r.pinner != nil {
+ r.pinner.Unpin()
+ r.pinner = nil
+ }
return nil
}
@@ -153,11 +175,26 @@
// Decode decodes Brotli encoded data.
func Decode(encodedData []byte) ([]byte, error) {
+ return DecodeWithRawDictionary(encodedData, nil)
+}
+
+// DecodeWithRawDictionary decodes Brotli encoded data with shared dictionary.
+func DecodeWithRawDictionary(encodedData []byte, dictionary []byte) ([]byte, error) {
+ s := C.BrotliDecoderCreateInstance(nil, nil, nil)
+ var p *runtime.Pinner
+ if dictionary != nil {
+ p = new(runtime.Pinner)
+ p.Pin(&dictionary[0])
+ // TODO(eustas): use return value
+ C.BrotliDecoderAttachDictionary(s, C.BrotliSharedDictionaryType( /* RAW */ 0),
+ C.size_t(len(dictionary)), (*C.uint8_t)(&dictionary[0]))
+ }
r := &Reader{
- src: bytes.NewReader(nil),
- state: C.BrotliDecoderCreateInstance(nil, nil, nil),
- buf: make([]byte, 4), // arbitrarily small but nonzero so that r.src.Read returns io.EOF
- in: encodedData,
+ src: bytes.NewReader(nil),
+ state: s,
+ buf: make([]byte, 4), // arbitrarily small but nonzero so that r.src.Read returns io.EOF
+ in: encodedData,
+ pinner: p,
}
defer r.Close()
return ioutil.ReadAll(r)
diff --git a/go/cbrotli/writer.go b/go/cbrotli/writer.go
index 44575fc..e1ea467 100644
--- a/go/cbrotli/writer.go
+++ b/go/cbrotli/writer.go
@@ -45,9 +45,54 @@
"bytes"
"errors"
"io"
+ "runtime"
"unsafe"
)
+// PreparedDictionary is a handle to native object.
+type PreparedDictionary struct {
+ opaque *C.BrotliEncoderPreparedDictionary
+ pinner *runtime.Pinner
+}
+
+// DictionaryType is type for shared dictionary
+type DictionaryType int
+
+const (
+ // DtRaw denotes LZ77 prefix dictionary
+ DtRaw DictionaryType = 0
+ // DtSerialized denotes serialized format
+ DtSerialized DictionaryType = 1
+)
+
+// NewPreparedDictionary prepares dictionary data for encoder.
+// Same instance can be used for multiple encoding sessions.
+// Close MUST be called to free resources.
+func NewPreparedDictionary(data []byte, dictionaryType DictionaryType, quality int) *PreparedDictionary {
+ var ptr *C.uint8_t
+ if len(data) != 0 {
+ ptr = (*C.uint8_t)(&data[0])
+ }
+ p := new(runtime.Pinner)
+ p.Pin(&data[0])
+ d := C.BrotliEncoderPrepareDictionary(C.BrotliSharedDictionaryType(dictionaryType), C.size_t(len(data)), ptr, C.int(quality), nil, nil, nil)
+ return &PreparedDictionary{
+ opaque: d,
+ pinner: p,
+ }
+}
+
+// Close frees C resources.
+// IMPORTANT: calling Close until all encoders that use that dictionary are closed as well will
+// cause crash.
+func (p *PreparedDictionary) Close() error {
+ // C-Brotli tolerates `nil` pointer here.
+ C.BrotliEncoderDestroyPreparedDictionary(p.opaque)
+ p.opaque = nil
+ p.pinner.Unpin()
+ return nil
+}
+
// WriterOptions configures Writer.
type WriterOptions struct {
// Quality controls the compression-speed vs compression-density trade-offs.
@@ -56,38 +101,56 @@
// LGWin is the base 2 logarithm of the sliding window size.
// Range is 10 to 24. 0 indicates automatic configuration based on Quality.
LGWin int
+ // Prepared shared dictionary
+ Dictionary *PreparedDictionary
}
// Writer implements io.WriteCloser by writing Brotli-encoded data to an
// underlying Writer.
type Writer struct {
+ healthy bool
dst io.Writer
state *C.BrotliEncoderState
buf, encoded []byte
}
var (
- errEncode = errors.New("cbrotli: encode error")
- errWriterClosed = errors.New("cbrotli: Writer is closed")
+ errEncode = errors.New("cbrotli: encode error")
+ errWriterClosed = errors.New("cbrotli: Writer is closed")
+ errWriterUnhealthy = errors.New("cbrotli: Writer is unhealthy")
)
// NewWriter initializes new Writer instance.
// Close MUST be called to free resources.
func NewWriter(dst io.Writer, options WriterOptions) *Writer {
state := C.BrotliEncoderCreateInstance(nil, nil, nil)
- C.BrotliEncoderSetParameter(
- state, C.BROTLI_PARAM_QUALITY, (C.uint32_t)(options.Quality))
+ healthy := state != nil
+ if C.BrotliEncoderSetParameter(
+ state, C.BROTLI_PARAM_QUALITY, (C.uint32_t)(options.Quality)) == 0 {
+ healthy = false
+ }
if options.LGWin > 0 {
- C.BrotliEncoderSetParameter(
- state, C.BROTLI_PARAM_LGWIN, (C.uint32_t)(options.LGWin))
+ if C.BrotliEncoderSetParameter(
+ state, C.BROTLI_PARAM_LGWIN, (C.uint32_t)(options.LGWin)) == 0 {
+ healthy = false
+ }
+ }
+ if options.Dictionary != nil {
+ if C.BrotliEncoderAttachPreparedDictionary(state, options.Dictionary.opaque) == 0 {
+ healthy = false
+ }
}
return &Writer{
- dst: dst,
- state: state,
+ healthy: healthy,
+ dst: dst,
+ state: state,
}
}
func (w *Writer) writeChunk(p []byte, op C.BrotliEncoderOperation) (n int, err error) {
+ if !w.healthy {
+ return 0, errWriterUnhealthy
+ }
if w.state == nil {
return 0, errWriterClosed
}