Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Encode methods accept []byte #36

Draft
wants to merge 57 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
cd91cc0
wip
noisersup Sep 4, 2024
0768049
wip
noisersup Sep 4, 2024
9e1e3ad
wip
noisersup Sep 4, 2024
ffbce94
keep
noisersup Sep 4, 2024
05fd355
Revert "keep"
noisersup Sep 4, 2024
c24cdf9
wip
noisersup Sep 4, 2024
9aa72ad
cap
noisersup Sep 6, 2024
116bd79
oof
noisersup Sep 6, 2024
0ba5399
wip
noisersup Sep 6, 2024
4388911
wip
noisersup Sep 6, 2024
6d3425d
wip
noisersup Sep 6, 2024
72494e2
wip
noisersup Sep 7, 2024
d1ebbaf
wip
noisersup Sep 7, 2024
c181cd3
wip
noisersup Sep 7, 2024
dd0a228
wip
noisersup Sep 9, 2024
99820cb
wip
noisersup Sep 11, 2024
7d65b15
revert
noisersup Sep 12, 2024
b26d225
Merge branch 'doc-encode-byte-21-2' into doc-encode-byte-21
noisersup Sep 12, 2024
1346fe2
wip
noisersup Sep 12, 2024
a08e5a1
wip
noisersup Sep 12, 2024
dcf1b1b
wip
noisersup Sep 12, 2024
5f28ffc
wip
noisersup Sep 12, 2024
9546de2
wip
noisersup Sep 12, 2024
b932258
wip
noisersup Sep 12, 2024
702798a
wip
noisersup Sep 13, 2024
f5d82af
write directly to subslice
noisersup Sep 13, 2024
7076440
wip
noisersup Sep 13, 2024
1155ef2
wip
noisersup Sep 13, 2024
076b324
remove writeByte
noisersup Sep 13, 2024
7d115af
refactor write
noisersup Sep 13, 2024
20e8069
wip
noisersup Sep 13, 2024
5e5e73e
wip
noisersup Sep 13, 2024
5d45057
convert array
noisersup Sep 13, 2024
73836e3
test
noisersup Sep 13, 2024
5ec1fc4
bench
noisersup Sep 13, 2024
7f13077
wip
noisersup Sep 13, 2024
8dd7397
wip
noisersup Sep 13, 2024
1a4d945
wip
noisersup Sep 13, 2024
f6151af
wip
noisersup Sep 13, 2024
70c05bb
wip
noisersup Sep 13, 2024
c84c84a
less error
noisersup Sep 13, 2024
c87f7ab
simplify test
noisersup Sep 13, 2024
b7510cd
wip
noisersup Sep 13, 2024
5aa5871
wip
noisersup Sep 13, 2024
b231449
Merge remote-tracking branch 'upstream/main' into doc-encode-byte-21
noisersup Sep 13, 2024
b67c25b
test
noisersup Sep 13, 2024
28ccc75
fix bench
noisersup Sep 13, 2024
fa03706
Merge remote-tracking branch 'upstream/main' into doc-encode-byte-21
noisersup Sep 16, 2024
8620b82
Merge branch 'main' into doc-encode-byte-21
AlekSi Sep 19, 2024
e9b08a7
Add notes
AlekSi Sep 19, 2024
58e30d9
Rename method
AlekSi Dec 16, 2024
a519e92
Add transition methods
AlekSi Dec 16, 2024
7fc88db
More transitions
AlekSi Dec 16, 2024
aff88f3
Revert some changes
AlekSi Dec 16, 2024
bcbad32
Revert
AlekSi Dec 16, 2024
49b0fd0
Merge branch 'main' into pr/noisersup/36-1
AlekSi Dec 16, 2024
ed4fe55
Rename
AlekSi Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ tasks:
bench:
desc: "Run benchmarks"
cmds:
- go test -list='{{.BENCH}}' ./wirebson
- go test -list='Benchmark.*' ./...
# -timeout is needed due to https://github.com/golang/go/issues/69181
- go test -bench='{{.BENCH}}' -count={{.BENCH_COUNT}} -benchtime={{.BENCH_TIME}} -timeout=60m ./wirebson | tee new.txt
- go test -count=10 -bench=BenchmarkDocument -benchtime={{.BENCH_TIME}} -timeout=60m ./wirebson | tee -a new.txt
- bin/benchstat old.txt new.txt

fuzz:
Expand Down
41 changes: 25 additions & 16 deletions wirebson/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package wirebson

import (
"bytes"
"encoding/binary"
"iter"
"log/slog"
Expand Down Expand Up @@ -150,31 +149,41 @@
}

// Encode encodes non-nil Array.
//
// TODO https://github.com/FerretDB/wire/issues/21
// This method should accept a slice of bytes, not return it.
// That would allow to avoid unnecessary allocations.
func (arr *Array) Encode() (RawArray, error) {
must.NotBeZero(arr)

size := sizeArray(arr)
buf := bytes.NewBuffer(make([]byte, 0, size))

if err := binary.Write(buf, binary.LittleEndian, uint32(size)); err != nil {
raw := make([]byte, Size(arr))
if err := arr.EncodeTo(raw); err != nil {

Check warning on line 156 in wirebson/array.go

View check run for this annotation

Codecov / codecov/patch

wirebson/array.go#L155-L156

Added lines #L155 - L156 were not covered by tests
return nil, lazyerrors.Error(err)
}

for i, v := range arr.values {
if err := encodeField(buf, strconv.Itoa(i), v); err != nil {
return nil, lazyerrors.Error(err)
return raw, nil

Check warning on line 160 in wirebson/array.go

View check run for this annotation

Codecov / codecov/patch

wirebson/array.go#L160

Added line #L160 was not covered by tests
}

// EncodeTo encodes non-nil Array.
//
// raw must be at least Size(arr) bytes long; otherwise, EncodeTo will panic.
// Only raw[0:Size(arr)] bytes are modified.
func (arr *Array) EncodeTo(raw RawArray) error {
must.NotBeZero(arr)

// ensure raw length early
s := sizeArray(arr)
raw[s-1] = 0

binary.LittleEndian.PutUint32(raw, uint32(s))

i := 4
for n, v := range arr.values {
w, err := encodeField(raw[i:], strconv.Itoa(n), v)
if err != nil {
return lazyerrors.Error(err)

Check warning on line 180 in wirebson/array.go

View check run for this annotation

Codecov / codecov/patch

wirebson/array.go#L180

Added line #L180 was not covered by tests
}
}

if err := binary.Write(buf, binary.LittleEndian, byte(0)); err != nil {
return nil, lazyerrors.Error(err)
i += w
}

return buf.Bytes(), nil
return nil
}

// Decode returns itself to implement [AnyArray].
Expand Down
39 changes: 24 additions & 15 deletions wirebson/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package wirebson

import (
"bytes"
"encoding/binary"
"iter"
"log/slog"
Expand Down Expand Up @@ -226,31 +225,41 @@
}

// Encode encodes non-nil Document.
//
// TODO https://github.com/FerretDB/wire/issues/21
// This method should accept a slice of bytes, not return it.
// That would allow to avoid unnecessary allocations.
func (doc *Document) Encode() (RawDocument, error) {
must.NotBeZero(doc)

size := sizeDocument(doc)
buf := bytes.NewBuffer(make([]byte, 0, size))

if err := binary.Write(buf, binary.LittleEndian, uint32(size)); err != nil {
raw := make([]byte, Size(doc))
if err := doc.EncodeTo(raw); err != nil {
return nil, lazyerrors.Error(err)
}

return raw, nil
}

// EncodeTo encodes non-nil Document.
//
// raw must be at least Size(doc) bytes long; otherwise, EncodeTo will panic.
// Only raw[0:Size(doc)] bytes are modified.
func (doc *Document) EncodeTo(raw RawDocument) error {
must.NotBeZero(doc)

// ensure raw length early
s := sizeDocument(doc)
raw[s-1] = 0

binary.LittleEndian.PutUint32(raw, uint32(s))

i := 4
for _, f := range doc.fields {
if err := encodeField(buf, f.name, f.value); err != nil {
return nil, lazyerrors.Error(err)
w, err := encodeField(raw[i:], f.name, f.value)
if err != nil {
return lazyerrors.Error(err)

Check warning on line 256 in wirebson/document.go

View check run for this annotation

Codecov / codecov/patch

wirebson/document.go#L256

Added line #L256 was not covered by tests
}
}

if err := binary.Write(buf, binary.LittleEndian, byte(0)); err != nil {
return nil, lazyerrors.Error(err)
i += w
}

return buf.Bytes(), nil
return nil
}

// Decode returns itself to implement [AnyDocument].
Expand Down
132 changes: 54 additions & 78 deletions wirebson/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package wirebson

import (
"bytes"
"fmt"
"time"

Expand All @@ -24,138 +23,115 @@

// encodeField encodes document/array field.
//
// It returns the number of bytes written.
// It panics if v is not a valid type.
func encodeField(buf *bytes.Buffer, name string, v any) error {
func encodeField(b []byte, name string, v any) (int, error) {
var i int
switch v := v.(type) {
case *Document:
if err := buf.WriteByte(byte(tagDocument)); err != nil {
return lazyerrors.Error(err)
}

b := make([]byte, SizeCString(name))
EncodeCString(b, name)
b[i] = byte(tagDocument)
i++

if _, err := buf.Write(b); err != nil {
return lazyerrors.Error(err)
}
EncodeCString(b[i:], name)
i += SizeCString(name)

b, err := v.Encode()
err := v.EncodeTo(b[i:])
if err != nil {
return lazyerrors.Error(err)
return 0, lazyerrors.Error(err)

Check warning on line 40 in wirebson/encode.go

View check run for this annotation

Codecov / codecov/patch

wirebson/encode.go#L40

Added line #L40 was not covered by tests
}

if _, err = buf.Write(b); err != nil {
return lazyerrors.Error(err)
}
i += sizeDocument(v)

case RawDocument:
if err := buf.WriteByte(byte(tagDocument)); err != nil {
return lazyerrors.Error(err)
}
b[i] = byte(tagDocument)
i++

b := make([]byte, SizeCString(name))
EncodeCString(b, name)
EncodeCString(b[i:], name)
i += SizeCString(name)

if _, err := buf.Write(b); err != nil {
return lazyerrors.Error(err)
if len(v) > len(b[i:]) {
panic(fmt.Sprintf("length of b should be at least %d bytes, got %d", len(v), len(b[i:])))

Check warning on line 53 in wirebson/encode.go

View check run for this annotation

Codecov / codecov/patch

wirebson/encode.go#L53

Added line #L53 was not covered by tests
}

if _, err := buf.Write(v); err != nil {
return lazyerrors.Error(err)
}
i += copy(b[i:], v)

case *Array:
if err := buf.WriteByte(byte(tagArray)); err != nil {
return lazyerrors.Error(err)
}

b := make([]byte, SizeCString(name))
EncodeCString(b, name)
b[i] = byte(tagArray)
i++

if _, err := buf.Write(b); err != nil {
return lazyerrors.Error(err)
}
EncodeCString(b[i:], name)
i += SizeCString(name)

b, err := v.Encode()
err := v.EncodeTo(b[i:])
if err != nil {
return lazyerrors.Error(err)
return 0, lazyerrors.Error(err)

Check warning on line 67 in wirebson/encode.go

View check run for this annotation

Codecov / codecov/patch

wirebson/encode.go#L67

Added line #L67 was not covered by tests
}

if _, err = buf.Write(b); err != nil {
return lazyerrors.Error(err)
}
i += sizeArray(v)

case RawArray:
if err := buf.WriteByte(byte(tagArray)); err != nil {
return lazyerrors.Error(err)
}
b[i] = byte(tagArray)
i++

b := make([]byte, SizeCString(name))
EncodeCString(b, name)
EncodeCString(b[i:], name)
i += SizeCString(name)

if _, err := buf.Write(b); err != nil {
return lazyerrors.Error(err)
if len(v) > len(b[i:]) {
panic(fmt.Sprintf("length of b should be at least %d bytes, got %d", len(v), len(b[i:])))

Check warning on line 80 in wirebson/encode.go

View check run for this annotation

Codecov / codecov/patch

wirebson/encode.go#L80

Added line #L80 was not covered by tests
}

if _, err := buf.Write(v); err != nil {
return lazyerrors.Error(err)
}
i += copy(b[i:], v)

default:
return encodeScalarField(buf, name, v)
return i + encodeScalarField(b[i:], name, v), nil
}

return nil
return i, nil
}

// encodeScalarField encodes scalar document field.
//
// It returns the number of bytes written.
// It panics if v is not a scalar value.
func encodeScalarField(buf *bytes.Buffer, name string, v any) error {
func encodeScalarField(b []byte, name string, v any) int {
var i int
switch v := v.(type) {
case float64:
buf.WriteByte(byte(tagFloat64))
b[i] = byte(tagFloat64)
case string:
buf.WriteByte(byte(tagString))
b[i] = byte(tagString)
case Binary:
buf.WriteByte(byte(tagBinary))
b[i] = byte(tagBinary)
case ObjectID:
buf.WriteByte(byte(tagObjectID))
b[i] = byte(tagObjectID)
case bool:
buf.WriteByte(byte(tagBool))
b[i] = byte(tagBool)
case time.Time:
buf.WriteByte(byte(tagTime))
b[i] = byte(tagTime)
case NullType:
buf.WriteByte(byte(tagNull))
b[i] = byte(tagNull)
case Regex:
buf.WriteByte(byte(tagRegex))
b[i] = byte(tagRegex)
case int32:
buf.WriteByte(byte(tagInt32))
b[i] = byte(tagInt32)
case Timestamp:
buf.WriteByte(byte(tagTimestamp))
b[i] = byte(tagTimestamp)
case int64:
buf.WriteByte(byte(tagInt64))
b[i] = byte(tagInt64)
case Decimal128:
buf.WriteByte(byte(tagDecimal128))
b[i] = byte(tagDecimal128)
default:
panic(fmt.Sprintf("invalid BSON type %T", v))
}
i++

b := make([]byte, SizeCString(name))
EncodeCString(b, name)
EncodeCString(b[i:], name)
i += SizeCString(name)

if _, err := buf.Write(b); err != nil {
return lazyerrors.Error(err)
}

b = make([]byte, sizeScalar(v))
encodeScalarValue(b, v)

if _, err := buf.Write(b); err != nil {
return lazyerrors.Error(err)
}
encodeScalarValue(b[i:], v)
i += sizeScalar(v)

return nil
return i
}

// encodeScalarValue encodes value v into b.
Expand Down
42 changes: 42 additions & 0 deletions wirebson/encode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package wirebson

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEncodeScalarField(t *testing.T) {
t.Parallel()

actual := make([]byte, 13)
assert.Equal(t, 13, encodeScalarField(actual[0:], "foo", "bar"))

expected := []byte{0x02, 0x66, 0x6f, 0x6f, 0x0, 0x4, 0x0, 0x0, 0x0, 0x62, 0x61, 0x72, 0x0}
assert.Equal(t, expected, actual)
}

func TestEncodeField(t *testing.T) {
t.Parallel()

var i int
actual := make([]byte, 22)
w, err := encodeField(actual[i:], "foo", "bar")
require.NoError(t, err)

assert.Equal(t, 13, w)
i += w

expected := []byte{0x2, 0x66, 0x6f, 0x6f, 0x0, 0x4, 0x0, 0x0, 0x0, 0x62, 0x61, 0x72, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
assert.Equal(t, expected, actual)

w, err = encodeField(actual[i:], "foo", int32(1))
require.NoError(t, err)

assert.Equal(t, 9, w)
i += w

expected = []byte{0x2, 0x66, 0x6f, 0x6f, 0x0, 0x4, 0x0, 0x0, 0x0, 0x62, 0x61, 0x72, 0x0, 0x10, 0x66, 0x6f, 0x6f, 0x0, 0x1, 0x0, 0x0, 0x0}
assert.Equal(t, expected, actual)
}
2 changes: 2 additions & 0 deletions wirebson/objectid.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ const sizeObjectID = 12
// b must be at least 12 ([sizeObjectID]) bytes long; otherwise, encodeObjectID will panic.
// Only b[0:12] bytes are modified.
func encodeObjectID(b []byte, v ObjectID) {
// ensure b length early
_ = b[11]

copy(b, v[:])
}

Expand Down