Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cristalhq/bson
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.0.7
Choose a base ref
...
head repository: cristalhq/bson
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 3 commits
  • 5 files changed
  • 2 contributors

Commits on Jan 2, 2024

  1. Add cstring helpers (#40)

    AlekSi authored Jan 2, 2024
    Copy the full SHA
    ad00c98 View commit details

Commits on Mar 8, 2024

  1. ci: bump cristalhq/.github from 0.7.0 to 0.8.1 (#42)

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Mar 8, 2024
    Copy the full SHA
    140d6a0 View commit details

Commits on Apr 4, 2024

  1. Copy the full SHA
    60edcbe View commit details
Showing with 148 additions and 17 deletions.
  1. +2 −2 .github/workflows/build.yml
  2. +12 −4 bsonproto/bsonproto.go
  3. +55 −11 bsonproto/bsonproto_test.go
  4. +39 −0 bsonproto/cstring.go
  5. +40 −0 bsonproto/decimal128.go
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ on:
# See https://github.com/cristalhq/.github/.github/workflows
jobs:
build:
uses: cristalhq/.github/.github/workflows/build.yml@v0.7.0
uses: cristalhq/.github/.github/workflows/build.yml@v0.8.1

vuln:
uses: cristalhq/.github/.github/workflows/vuln.yml@v0.7.0
uses: cristalhq/.github/.github/workflows/vuln.yml@v0.8.1
16 changes: 12 additions & 4 deletions bsonproto/bsonproto.go
Original file line number Diff line number Diff line change
@@ -8,8 +8,10 @@ import (
)

// ScalarType represents a BSON scalar type.
//
// CString is not included as it is not a real BSON type.
type ScalarType interface {
float64 | string | Binary | ObjectID | bool | time.Time | NullType | Regex | int32 | Timestamp | int64
float64 | string | Binary | ObjectID | bool | time.Time | NullType | Regex | int32 | Timestamp | int64 | Decimal128
}

// Size returns a size of the encoding of value v in bytes.
@@ -19,7 +21,7 @@ func Size[T ScalarType](v T) int {

// SizeAny returns a size of the encoding of value v in bytes.
//
// It panics if v is not a [ScalarType].
// It panics if v is not a [ScalarType] (including CString).
func SizeAny(v any) int {
switch v := v.(type) {
case float64:
@@ -44,6 +46,8 @@ func SizeAny(v any) int {
return SizeTimestamp
case int64:
return SizeInt64
case Decimal128:
return SizeDecimal128
default:
panic(fmt.Sprintf("unsupported type %T", v))
}
@@ -62,7 +66,7 @@ func Encode[T ScalarType](b []byte, v T) {
// b must be at least Size(v) bytes long; otherwise, EncodeAny will panic.
// Only b[0:Size(v)] bytes are modified.
//
// It panics if v is not a [ScalarType].
// It panics if v is not a [ScalarType] (including CString).
func EncodeAny(b []byte, v any) {
switch v := v.(type) {
case float64:
@@ -87,6 +91,8 @@ func EncodeAny(b []byte, v any) {
EncodeTimestamp(b, v)
case int64:
EncodeInt64(b, v)
case Decimal128:
EncodeDecimal128(b, v)
default:
panic(fmt.Sprintf("unsupported type %T", v))
}
@@ -105,7 +111,7 @@ func Decode[T ScalarType](b []byte, v *T) error {
// If there is not enough bytes, DecodeAny will return a wrapped [ErrDecodeShortInput].
// If the input is otherwise invalid, a wrapped [ErrDecodeInvalidInput] is returned.
//
// It panics if v is not a pointer to [ScalarType].
// It panics if v is not a pointer to [ScalarType] (including CString).
func DecodeAny(b []byte, v any) error {
var err error
switch v := v.(type) {
@@ -131,6 +137,8 @@ func DecodeAny(b []byte, v any) error {
*v, err = DecodeTimestamp(b)
case *int64:
*v, err = DecodeInt64(b)
case *Decimal128:
*v, err = DecodeDecimal128(b)
default:
panic(fmt.Sprintf("unsupported type %T", v))
}
66 changes: 55 additions & 11 deletions bsonproto/bsonproto_test.go
Original file line number Diff line number Diff line change
@@ -71,28 +71,31 @@ func TestScalars(t *testing.T) {
}, {
v: int64(1234567890123456789),
b: []byte{0x15, 0x81, 0xe9, 0x7d, 0xf4, 0x10, 0x22, 0x11},
}, {
v: Decimal128{H: 2024, L: 404},
b: []byte{0x94, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}} {
t.Run(fmt.Sprintf("%[1]d_%[2]T(%[2]v)", i, tc.v), func(t *testing.T) {
s := SizeAny(tc.v)
if s != len(tc.b) {
t.Fatalf("Size(%[1]T(%[1]v)) = %[2]d, expected %[3]d", tc.v, s, len(tc.b))
t.Fatalf("SizeAny(%[1]T(%[1]v)) = %[2]d, expected %[3]d", tc.v, s, len(tc.b))
}

actualB := make([]byte, s)
EncodeAny(actualB, tc.v)
if !bytes.Equal(actualB, tc.b) {
t.Errorf("Encode(%[1]T(%[1]v))\n actual %#[2]v\n expected %#[3]v", tc.v, actualB, tc.b)
t.Errorf("EncodeAny(%[1]T(%[1]v))\n actual %#[2]v\n expected %#[3]v", tc.v, actualB, tc.b)
}

actualV := reflect.New(reflect.TypeOf(tc.v)).Interface() // actualV := new(T)
err := DecodeAny(actualB, actualV)
if err != nil {
t.Fatalf("Decode(%v): %s", actualB, err)
t.Fatalf("DecodeAny(%v): %s", actualB, err)
}

actualV = reflect.ValueOf(actualV).Elem().Interface() // *actualV
if !reflect.DeepEqual(actualV, tc.v) {
t.Errorf("Decode(%v)\n actual %v\n expected %v", actualB, actualV, tc.v)
t.Errorf("DecodeAny(%v)\n actual %v\n expected %v", actualB, actualV, tc.v)
}
})
}
@@ -106,15 +109,15 @@ func TestFloat64(t *testing.T) {
actualB := make([]byte, 8)
EncodeFloat64(actualB, v)
if !bytes.Equal(actualB, b) {
t.Errorf("Encode(%[1]T(%[1]v)) = %#[2]v, expected %#[3]v", v, actualB, b)
t.Errorf("EncodeFloat64(%[1]T(%[1]v)) = %#[2]v, expected %#[3]v", v, actualB, b)
}

actualV, err := DecodeFloat64(actualB)
if err != nil {
t.Fatalf("Decode(%v): %s", actualB, err)
t.Fatalf("DecodeFloat64(%v): %s", actualB, err)
}
if !reflect.DeepEqual(actualV, v) || !math.Signbit(actualV) {
t.Errorf("Decode(%v) = %v, expected %v", actualB, actualV, v)
t.Errorf("DecodeFloat64(%v) = %v, expected %v", actualB, actualV, v)
}
})

@@ -144,21 +147,58 @@ func TestFloat64(t *testing.T) {
actualB := make([]byte, 8)
EncodeFloat64(actualB, tc.v)
if !bytes.Equal(actualB, tc.b) {
t.Errorf("Encode(%[1]T(%[1]v)) = %#[2]v, expected %#[3]v", tc.v, actualB, tc.b)
t.Errorf("EncodeFloat64(%[1]T(%[1]v)) = %#[2]v, expected %#[3]v", tc.v, actualB, tc.b)
}

actualV, err := DecodeFloat64(actualB)
if err != nil {
t.Fatalf("Decode(%v): %s", actualB, err)
t.Fatalf("DecodeFloat64(%v): %s", actualB, err)
}
if !math.IsNaN(actualV) {
t.Errorf("Decode(%v) = %v, expected NaN", actualB, actualV)
t.Errorf("DecodeFloat64(%v) = %v, expected NaN", actualB, actualV)
}
})
}
})
}

func TestCString(t *testing.T) {
for _, tc := range []struct {
v string
b []byte
}{{
v: "foo",
b: []byte{0x66, 0x6f, 0x6f, 0x00},
}, {
v: "f",
b: []byte{0x66, 0x00},
}, {
v: "",
b: []byte{0x00},
}} {
t.Run(tc.v, func(t *testing.T) {
s := SizeCString(tc.v)
if s != len(tc.b) {
t.Fatalf("SizeCString(%[1]T(%[1]v)) = %[2]d, expected %[3]d", tc.v, s, len(tc.b))
}

actualB := make([]byte, s)
EncodeCString(actualB, tc.v)
if !bytes.Equal(actualB, tc.b) {
t.Errorf("EncodeCString(%[1]q)\n actual %#[2]v\n expected %#[3]v", tc.v, actualB, tc.b)
}

actualV, err := DecodeCString(actualB)
if err != nil {
t.Fatalf("DecodeCString(%v): %s", actualB, err)
}
if actualV != tc.v {
t.Errorf("DecodeCString(%v) = %q, expected %q", actualB, actualV, tc.v)
}
})
}
}

func TestScalarsDecodeErrors(t *testing.T) {
for i, tc := range []struct {
b []byte
@@ -232,12 +272,16 @@ func TestScalarsDecodeErrors(t *testing.T) {
b: []byte{0x42},
v: int64(0),
err: ErrDecodeShortInput,
}, {
b: []byte{0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42},
v: Decimal128{},
err: ErrDecodeShortInput,
}} {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
v := reflect.New(reflect.TypeOf(tc.v)).Interface() // v := new(T)
err := DecodeAny(tc.b, v)
if !errors.Is(err, tc.err) {
t.Errorf("Decode(%v): %v, expected %v", tc.b, err, tc.err)
t.Errorf("DecodeAny(%v): %v, expected %v", tc.b, err, tc.err)
}
})
}
39 changes: 39 additions & 0 deletions bsonproto/cstring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package bsonproto

import (
"bytes"
"fmt"
)

// SizeCString returns a size of the encoding of v cstring in bytes.
func SizeCString(v string) int {
return len(v) + 1
}

// EncodeCString encodes cstring value v into b.
//
// b must be at least len(v)+1 ([SizeCString]) bytes long; otherwise, EncodeString will panic.
// Only b[0:len(v)+1] bytes are modified.
func EncodeCString(b []byte, v string) {
// ensure b length early
b[len(v)] = 0

copy(b, v)
}

// DecodeCString decodes cstring value from b.
//
// If there is not enough bytes, DecodeCString will return a wrapped [ErrDecodeShortInput].
// If the input is otherwise invalid, a wrapped [ErrDecodeInvalidInput] is returned.
func DecodeCString(b []byte) (string, error) {
if len(b) < 1 {
return "", fmt.Errorf("DecodeCString: expected at least 1 byte, got %d: %w", len(b), ErrDecodeShortInput)
}

i := bytes.IndexByte(b, 0)
if i == -1 {
return "", fmt.Errorf("DecodeCString: expected to find 0 byte: %w", ErrDecodeInvalidInput)
}

return string(b[:i]), nil
}
40 changes: 40 additions & 0 deletions bsonproto/decimal128.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package bsonproto

import (
"encoding/binary"
"fmt"
)

// Decimal128 represents BSON scalar type decimal128.
type Decimal128 struct {
L uint64
H uint64
}

// SizeDecimal128 is a size of the encoding of [Decimal128] in bytes.
const SizeDecimal128 = 16

// EncodeDecimal128 encodes [Decimal128] value v into b.
//
// b must be at least 16 ([SizeDecimal128]) bytes long; otherwise, EncodeDecimal128 will panic.
// Only b[0:16] bytes are modified.
func EncodeDecimal128(b []byte, v Decimal128) {
binary.LittleEndian.PutUint64(b, uint64(v.L))
binary.LittleEndian.PutUint64(b[8:], uint64(v.H))
}

// DecodeDecimal128 decodes [Decimal128] value from b.
//
// If there is not enough bytes, DecodeDecimal128 will return a wrapped [ErrDecodeShortInput].
func DecodeDecimal128(b []byte) (Decimal128, error) {
var res Decimal128

if len(b) < SizeDecimal128 {
return res, fmt.Errorf("DecodeDecimal128: expected at least %d bytes, got %d: %w", SizeDecimal128, len(b), ErrDecodeShortInput)
}

res.L = binary.LittleEndian.Uint64(b[:8])
res.H = binary.LittleEndian.Uint64(b[8:])

return res, nil
}