diff --git a/bsonproto/binary.go b/bsonproto/binary.go index 5120465..befd7f6 100644 --- a/bsonproto/binary.go +++ b/bsonproto/binary.go @@ -1,3 +1,86 @@ package bsonproto -type Binary struct{} +import ( + "encoding/binary" + "fmt" +) + +//go:generate go run golang.org/x/tools/cmd/stringer@latest -linecomment -type BinarySubtype + +// BinarySubtype represents BSON Binary's subtype. +type BinarySubtype byte + +const ( + // BinaryGeneric represents a BSON Binary generic subtype. + BinaryGeneric = BinarySubtype(0x00) // generic + + // BinaryFunction represents a BSON Binary function subtype + BinaryFunction = BinarySubtype(0x01) // function + + // BinaryGenericOld represents a BSON Binary generic-old subtype. + BinaryGenericOld = BinarySubtype(0x02) // generic-old + + // BinaryUUIDOld represents a BSON Binary UUID old subtype. + BinaryUUIDOld = BinarySubtype(0x03) // uuid-old + + // BinaryUUID represents a BSON Binary UUID subtype. + BinaryUUID = BinarySubtype(0x04) // uuid + + // BinaryMD5 represents a BSON Binary MD5 subtype. + BinaryMD5 = BinarySubtype(0x05) // md5 + + // BinaryEncrypted represents a BSON Binary encrypted subtype. + BinaryEncrypted = BinarySubtype(0x06) // encrypted + + // BinaryUser represents a BSON Binary user-defined subtype. + BinaryUser = BinarySubtype(0x80) // user +) + +// Binary represents BSON scalar type binary. +type Binary struct { + B []byte + Subtype BinarySubtype +} + +// SizeBinary returns a size of the encoding of v [Binary] in bytes. +func SizeBinary(v Binary) int { + return len(v.B) + 5 +} + +// EncodeBinary encodes [Binary] value v into b. +// +// b must be at least len(v.B)+5 ([SizeBinary]) bytes long; otherwise, EncodeBinary will panic. +// Only b[0:len(v.B)+5] bytes are modified. +func EncodeBinary(b []byte, v Binary) { + i := len(v.B) + + binary.LittleEndian.PutUint32(b, uint32(i)) + b[4] = byte(v.Subtype) + copy(b[5:5+i], v.B) +} + +// DecodeBinary decodes [Binary] value from b. +// +// If there is not enough bytes, DecodeBinary will return a wrapped [ErrDecodeShortInput]. +// If the input is otherwise invalid, a wrapped [ErrDecodeInvalidInput] is returned. +func DecodeBinary(b []byte) (Binary, error) { + var res Binary + + if len(b) < 5 { + return res, fmt.Errorf("DecodeBinary: expected at least 5 bytes, got %d: %w", len(b), ErrDecodeShortInput) + } + + i := int(binary.LittleEndian.Uint32(b)) + if e := 5 + i; len(b) < e { + return res, fmt.Errorf("DecodeBinary: expected at least %d bytes, got %d: %w", e, len(b), ErrDecodeShortInput) + } + + res.Subtype = BinarySubtype(b[4]) + + if i > 0 { + res.B = make([]byte, i) + copy(res.B, b[5:5+i]) + } + + return res, nil +} diff --git a/bsonproto/binarysubtype_string.go b/bsonproto/binarysubtype_string.go new file mode 100644 index 0000000..13130d8 --- /dev/null +++ b/bsonproto/binarysubtype_string.go @@ -0,0 +1,39 @@ +// Code generated by "stringer -linecomment -type BinarySubtype"; DO NOT EDIT. + +package bsonproto + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[BinaryGeneric-0] + _ = x[BinaryFunction-1] + _ = x[BinaryGenericOld-2] + _ = x[BinaryUUIDOld-3] + _ = x[BinaryUUID-4] + _ = x[BinaryMD5-5] + _ = x[BinaryEncrypted-6] + _ = x[BinaryUser-128] +} + +const ( + _BinarySubtype_name_0 = "genericfunctiongeneric-olduuid-olduuidmd5encrypted" + _BinarySubtype_name_1 = "user" +) + +var ( + _BinarySubtype_index_0 = [...]uint8{0, 7, 15, 26, 34, 38, 41, 50} +) + +func (i BinarySubtype) String() string { + switch { + case i <= 6: + return _BinarySubtype_name_0[_BinarySubtype_index_0[i]:_BinarySubtype_index_0[i+1]] + case i == 128: + return _BinarySubtype_name_1 + default: + return "BinarySubtype(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/bsonproto/bool.go b/bsonproto/bool.go index 701fca7..566729c 100644 --- a/bsonproto/bool.go +++ b/bsonproto/bool.go @@ -1 +1,36 @@ package bsonproto + +import "fmt" + +// SizeBool is a size of the encoding of bool in bytes. +const SizeBool = 1 + +// EncodeBool encodes bool value v into b. +// +// b must be at least 1 ([SizeBool]) byte long; otherwise, EncodeBool will panic. +// Only b[0] is modified. +func EncodeBool(b []byte, v bool) { + if v { + b[0] = 0x01 + } else { + b[0] = 0x00 + } +} + +// DecodeBool decodes bool value from b. +// +// If there is not enough bytes, DecodeBool will return a wrapped [ErrDecodeShortInput]. +func DecodeBool(b []byte) (bool, error) { + if len(b) == 0 { + return false, fmt.Errorf("DecodeBool: expected at least 1 byte, got 0: %w", ErrDecodeShortInput) + } + + switch b[0] { + case 0x00: + return false, nil + case 0x01: + return true, nil + default: + return false, fmt.Errorf("DecodeBool: expected 0x00 or 0x01, got 0x%02x: %w", b[0], ErrDecodeInvalidInput) + } +} diff --git a/bsonproto/bsonproto.go b/bsonproto/bsonproto.go index beee1ba..9e463ac 100644 --- a/bsonproto/bsonproto.go +++ b/bsonproto/bsonproto.go @@ -19,17 +19,31 @@ 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]. func SizeAny(v any) int { switch v := v.(type) { case float64: - return SizeFloat64(v) + return SizeFloat64 case string: return SizeString(v) + case Binary: + return SizeBinary(v) + case ObjectID: + return SizeObjectID + case bool: + return SizeBool + case time.Time: + return SizeTime + case NullType: + return 0 + case Regex: + return SizeRegex(v) case int32: - return SizeInt32(v) + return SizeInt32 + case Timestamp: + return SizeTimestamp case int64: - return SizeInt64(v) + return SizeInt64 default: panic(fmt.Sprintf("unsupported type %T", v)) } @@ -48,15 +62,29 @@ 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]. func EncodeAny(b []byte, v any) { switch v := v.(type) { case float64: EncodeFloat64(b, v) case string: EncodeString(b, v) + case Binary: + EncodeBinary(b, v) + case ObjectID: + EncodeObjectID(b, v) + case bool: + EncodeBool(b, v) + case time.Time: + EncodeTime(b, v) + case NullType: + // nothing + case Regex: + EncodeRegex(b, v) case int32: EncodeInt32(b, v) + case Timestamp: + EncodeTimestamp(b, v) case int64: EncodeInt64(b, v) default: @@ -66,20 +94,18 @@ func EncodeAny(b []byte, v any) { // Decode decodes value from b into v. // -// If there is not enough bytes, Decode will return a wrapped ErrDecodeShortInput. -// If the input is otherwise invalid, a wrapped ErrDecodeInvalidInput is returned. -// -// If the value can't be decoded, a wrapped ErrDecodeInvalidInput is returned. +// If there is not enough bytes, Decode will return a wrapped [ErrDecodeShortInput]. +// If the input is otherwise invalid, a wrapped [ErrDecodeInvalidInput] is returned. func Decode[T ScalarType](b []byte, v *T) error { return DecodeAny(b, v) } // DecodeAny decodes value from b into v. // -// If there is not enough bytes, DecodeAny will return a wrapped ErrDecodeShortInput. -// If the input is otherwise invalid, a wrapped ErrDecodeInvalidInput is returned. +// 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]. func DecodeAny(b []byte, v any) error { var err error switch v := v.(type) { @@ -87,8 +113,22 @@ func DecodeAny(b []byte, v any) error { *v, err = DecodeFloat64(b) case *string: *v, err = DecodeString(b) + case *Binary: + *v, err = DecodeBinary(b) + case *ObjectID: + *v, err = DecodeObjectID(b) + case *bool: + *v, err = DecodeBool(b) + case *time.Time: + *v, err = DecodeTime(b) + case *NullType: + // nothing + case *Regex: + *v, err = DecodeRegex(b) case *int32: *v, err = DecodeInt32(b) + case *Timestamp: + *v, err = DecodeTimestamp(b) case *int64: *v, err = DecodeInt64(b) default: diff --git a/bsonproto/bsonproto_test.go b/bsonproto/bsonproto_test.go index 6a763c4..bcd8b8b 100644 --- a/bsonproto/bsonproto_test.go +++ b/bsonproto/bsonproto_test.go @@ -7,6 +7,7 @@ import ( "math" "reflect" "testing" + "time" ) func TestScalars(t *testing.T) { @@ -14,17 +15,59 @@ func TestScalars(t *testing.T) { v any b []byte }{{ - v: 42.13, + v: float64(42.13), b: []byte{0x71, 0x3d, 0xa, 0xd7, 0xa3, 0x10, 0x45, 0x40}, }, { v: math.Inf(-1), b: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff}, }, { v: "foo", - b: []byte{0x08, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 0x00}, + b: []byte{0x04, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 0x00}, + }, { + v: "f", + b: []byte{0x02, 0x00, 0x00, 0x00, 0x66, 0x00}, + }, { + v: "", + b: []byte{0x01, 0x00, 0x00, 0x00, 0x00}, + }, { + v: Binary{B: []byte("foo"), Subtype: BinaryUser}, + b: []byte{0x03, 0x00, 0x00, 0x00, 0x80, 0x66, 0x6f, 0x6f}, + }, { + v: Binary{B: []byte("f"), Subtype: BinaryUser}, + b: []byte{0x01, 0x00, 0x00, 0x00, 0x80, 0x66}, + }, { + v: Binary{Subtype: BinaryUser}, + b: []byte{0x00, 0x00, 0x00, 0x00, 0x80}, + }, { + v: ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, + b: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, + }, { + v: false, + b: []byte{0x00}, + }, { + v: true, + b: []byte{0x01}, + }, { + v: time.Date(2023, 12, 26, 13, 22, 42, 123000000, time.UTC), + b: []byte{0x4b, 0xb1, 0x4a, 0xa6, 0x8c, 0x01, 0x00, 0x00}, + }, { + v: time.Unix(0, 0).UTC(), + b: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + }, { + v: time.Time{}, + b: []byte{0x00, 0x28, 0xd3, 0xed, 0x7c, 0xc7, 0xff, 0xff}, + }, { + v: Regex{Pattern: "foo", Options: "m"}, + b: []byte{0x66, 0x6f, 0x6f, 0x00, 0x6d, 0x00}, + }, { + v: Regex{Pattern: "", Options: ""}, + b: []byte{0x00, 0x00}, }, { v: int32(123456789), b: []byte{0x15, 0xcd, 0x5b, 0x07}, + }, { + v: Timestamp(1234567890123456789), + b: []byte{0x15, 0x81, 0xe9, 0x7d, 0xf4, 0x10, 0x22, 0x11}, }, { v: int64(1234567890123456789), b: []byte{0x15, 0x81, 0xe9, 0x7d, 0xf4, 0x10, 0x22, 0x11}, @@ -32,13 +75,13 @@ func TestScalars(t *testing.T) { 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, want %[3]d", tc.v, s, len(tc.b)) + t.Fatalf("Size(%[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)) = %#[2]v, want %#[3]v", tc.v, actualB, tc.b) + t.Errorf("Encode(%[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) @@ -49,7 +92,7 @@ func TestScalars(t *testing.T) { actualV = reflect.ValueOf(actualV).Elem().Interface() // *actualV if !reflect.DeepEqual(actualV, tc.v) { - t.Errorf("Decode(%v) = %v, want %v", actualB, actualV, tc.v) + t.Errorf("Decode(%v)\n actual %v\n expected %v", actualB, actualV, tc.v) } }) } @@ -63,7 +106,7 @@ 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, want %#[3]v", v, actualB, b) + t.Errorf("Encode(%[1]T(%[1]v)) = %#[2]v, expected %#[3]v", v, actualB, b) } actualV, err := DecodeFloat64(actualB) @@ -71,7 +114,7 @@ func TestFloat64(t *testing.T) { t.Fatalf("Decode(%v): %s", actualB, err) } if !reflect.DeepEqual(actualV, v) || !math.Signbit(actualV) { - t.Errorf("Decode(%v) = %v, want %v", actualB, actualV, v) + t.Errorf("Decode(%v) = %v, expected %v", actualB, actualV, v) } }) @@ -101,7 +144,7 @@ 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, want %#[3]v", tc.v, actualB, tc.b) + t.Errorf("Encode(%[1]T(%[1]v)) = %#[2]v, expected %#[3]v", tc.v, actualB, tc.b) } actualV, err := DecodeFloat64(actualB) @@ -109,7 +152,7 @@ func TestFloat64(t *testing.T) { t.Fatalf("Decode(%v): %s", actualB, err) } if !math.IsNaN(actualV) { - t.Errorf("Decode(%v) = %v, want NaN", actualB, actualV) + t.Errorf("Decode(%v) = %v, expected NaN", actualB, actualV) } }) } @@ -123,12 +166,68 @@ func TestScalarsDecodeErrors(t *testing.T) { err error }{{ b: []byte{0x42}, + v: float64(0), + err: ErrDecodeShortInput, + }, { + b: []byte{0x42}, + v: string(""), + err: ErrDecodeShortInput, + }, { + b: []byte{0x42, 0x42, 0x42, 0x42, 0x42}, + v: string(""), + err: ErrDecodeShortInput, + }, { + b: []byte{0x00, 0x00, 0x00, 0x00, 0x42}, v: string(""), + err: ErrDecodeInvalidInput, + }, { + b: []byte{0x01, 0x00, 0x00, 0x00, 0x42}, + v: string(""), + err: ErrDecodeInvalidInput, + }, { + b: []byte{0x42}, + v: Binary{}, + err: ErrDecodeShortInput, + }, { + b: []byte{0x01, 0x00, 0x00, 0x00, 0x80}, + v: Binary{}, + err: ErrDecodeShortInput, + }, { + b: []byte{}, + v: ObjectID{}, + err: ErrDecodeShortInput, + }, { + b: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}, + v: ObjectID{}, + err: ErrDecodeShortInput, + }, { + b: []byte{}, + v: false, + err: ErrDecodeShortInput, + }, { + b: []byte{0x42}, + v: false, + err: ErrDecodeInvalidInput, + }, { + b: []byte{0x42}, + v: time.Time{}, + err: ErrDecodeShortInput, + }, { + b: []byte{0x00}, + v: Regex{}, + err: ErrDecodeShortInput, + }, { + b: []byte{0x00, 0x42}, + v: Regex{}, err: ErrDecodeShortInput, }, { b: []byte{0x42}, v: int32(0), err: ErrDecodeShortInput, + }, { + b: []byte{0x42}, + v: Timestamp(0), + err: ErrDecodeShortInput, }, { b: []byte{0x42}, v: int64(0), @@ -138,7 +237,7 @@ func TestScalarsDecodeErrors(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, want %v", tc.b, err, tc.err) + t.Errorf("Decode(%v): %v, expected %v", tc.b, err, tc.err) } }) } diff --git a/bsonproto/float64.go b/bsonproto/float64.go index 031b907..c76ac82 100644 --- a/bsonproto/float64.go +++ b/bsonproto/float64.go @@ -6,16 +6,12 @@ import ( "math" ) -// SizeFloat64 returns a size of the encoding of float64 in bytes - 8. -// -// The argument is unused. -func SizeFloat64(float64) int { - return 8 -} +// SizeFloat64 is a size of the encoding of float64 in bytes. +const SizeFloat64 = 8 // EncodeFloat64 encodes float64 value v into b. // -// b must be at least 8 bytes long; otherwise, EncodeFloat64 will panic. +// b must be at least 8 ([SizeFloat64]) bytes long; otherwise, EncodeFloat64 will panic. // Only b[0:8] bytes are modified. // // Infinities, NaNs, negative zeros are preserved. @@ -25,12 +21,12 @@ func EncodeFloat64(b []byte, v float64) { // DecodeFloat64 decodes float64 value from b. // -// If there is not enough bytes, DecodeFloat64 will return a wrapped ErrDecodeShortInput. +// If there is not enough bytes, DecodeFloat64 will return a wrapped [ErrDecodeShortInput]. // // Infinities, NaNs, negative zeros are preserved. func DecodeFloat64(b []byte) (float64, error) { - if len(b) < 8 { - return 0, fmt.Errorf("DecodeFloat64: expected at least 8 bytes, got %d: %w", len(b), ErrDecodeShortInput) + if len(b) < SizeFloat64 { + return 0, fmt.Errorf("DecodeFloat64: expected at least %d bytes, got %d: %w", SizeFloat64, len(b), ErrDecodeShortInput) } return math.Float64frombits(binary.LittleEndian.Uint64(b)), nil diff --git a/bsonproto/int32.go b/bsonproto/int32.go index 8a5a1c0..d6c8221 100644 --- a/bsonproto/int32.go +++ b/bsonproto/int32.go @@ -5,16 +5,12 @@ import ( "fmt" ) -// SizeInt32 returns a size of the encoding of int32 in bytes - 4. -// -// The argument is unused. -func SizeInt32(int32) int { - return 4 -} +// SizeInt32 is a size of the encoding of int32 in bytes. +const SizeInt32 = 4 // EncodeInt32 encodes int32 value v into b. // -// b must be at least 4 bytes long; otherwise, EncodeInt32 will panic. +// b must be at least 4 ([SizeInt32]) bytes long; otherwise, EncodeInt32 will panic. // Only b[0:4] bytes are modified. func EncodeInt32(b []byte, v int32) { binary.LittleEndian.PutUint32(b, uint32(v)) @@ -22,10 +18,10 @@ func EncodeInt32(b []byte, v int32) { // DecodeInt32 decodes int32 value from b. // -// If there is not enough bytes, DecodeInt32 will return a wrapped ErrDecodeShortInput. +// If there is not enough bytes, DecodeInt32 will return a wrapped [ErrDecodeShortInput]. func DecodeInt32(b []byte) (int32, error) { - if len(b) < 4 { - return 0, fmt.Errorf("DecodeInt32: expected at least 4 bytes, got %d: %w", len(b), ErrDecodeShortInput) + if len(b) < SizeInt32 { + return 0, fmt.Errorf("DecodeInt32: expected at least %d bytes, got %d: %w", SizeInt32, len(b), ErrDecodeShortInput) } return int32(binary.LittleEndian.Uint32(b)), nil diff --git a/bsonproto/int64.go b/bsonproto/int64.go index d3d3de5..674a7af 100644 --- a/bsonproto/int64.go +++ b/bsonproto/int64.go @@ -5,16 +5,12 @@ import ( "fmt" ) -// SizeInt64 returns a size of the encoding of int64 in bytes - 8. -// -// The argument is unused. -func SizeInt64(int64) int { - return 8 -} +// SizeInt64 is a size of the encoding of int64 in bytes. +const SizeInt64 = 8 // EncodeInt64 encodes int64 value v into b. // -// b must be at least 8 bytes long; otherwise, EncodeInt64 will panic. +// b must be at least 8 ([SizeInt64]) bytes long; otherwise, EncodeInt64 will panic. // Only b[0:8] bytes are modified. func EncodeInt64(b []byte, v int64) { binary.LittleEndian.PutUint64(b, uint64(v)) @@ -22,10 +18,10 @@ func EncodeInt64(b []byte, v int64) { // DecodeInt64 decodes int64 value from b. // -// If there is not enough bytes, DecodeInt64 will return a wrapped ErrDecodeShortInput. +// If there is not enough bytes, DecodeInt64 will return a wrapped [ErrDecodeShortInput]. func DecodeInt64(b []byte) (int64, error) { - if len(b) < 8 { - return 0, fmt.Errorf("DecodeInt64: expected at least 8 bytes, got %d: %w", len(b), ErrDecodeShortInput) + if len(b) < SizeInt64 { + return 0, fmt.Errorf("DecodeInt64: expected at least %d bytes, got %d: %w", SizeInt64, len(b), ErrDecodeShortInput) } return int64(binary.LittleEndian.Uint64(b)), nil diff --git a/bsonproto/nulltype.go b/bsonproto/nulltype.go index a1d9e0f..14b0a29 100644 --- a/bsonproto/nulltype.go +++ b/bsonproto/nulltype.go @@ -1,5 +1,7 @@ package bsonproto +// NullType represents BSON scalar type null. type NullType struct{} +// Null represents BSON scalar value null. var Null = NullType{} diff --git a/bsonproto/objectid.go b/bsonproto/objectid.go index c5eb0e6..14ed687 100644 --- a/bsonproto/objectid.go +++ b/bsonproto/objectid.go @@ -1,3 +1,33 @@ package bsonproto -type ObjectID struct{} +import "fmt" + +// ObjectID represents BSON scalar type ObjectID. +type ObjectID [12]byte + +// SizeObjectID is a size of the encoding of [ObjectID] in bytes. +const SizeObjectID = 12 + +// EncodeObjectID encodes [ObjectID] value v into b. +// +// 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) { + _ = b[11] + copy(b, v[:]) +} + +// DecodeObjectID decodes [ObjectID] value from b. +// +// If there is not enough bytes, DecodeObjectID will return a wrapped [ErrDecodeShortInput]. +func DecodeObjectID(b []byte) (ObjectID, error) { + var res ObjectID + + if len(b) < SizeObjectID { + return res, fmt.Errorf("DecodeObjectID: expected at least %d bytes, got %d: %w", SizeObjectID, len(b), ErrDecodeShortInput) + } + + copy(res[:], b) + + return res, nil +} diff --git a/bsonproto/regex.go b/bsonproto/regex.go index 7d33612..84d8606 100644 --- a/bsonproto/regex.go +++ b/bsonproto/regex.go @@ -1,3 +1,62 @@ package bsonproto -type Regex struct{} +import ( + "fmt" +) + +// Regex represents BSON scalar type regular expression. +type Regex struct { + Pattern string + Options string +} + +// SizeRegex returns a size of the encoding of v [Regex] in bytes. +func SizeRegex(v Regex) int { + return len(v.Pattern) + len(v.Options) + 2 +} + +// EncodeRegex encodes [Regex] value v into b. +// +// b must be at least len(v.Pattern)+len(v.Options)+2 ([SizeRegex]) bytes long; otherwise, EncodeRegex will panic. +// Only b[0:len(v.Pattern)+len(v.Options)+2] bytes are modified. +func EncodeRegex(b []byte, v Regex) { + // ensure b length early + b[len(v.Pattern)+len(v.Options)+1] = 0 + + copy(b, v.Pattern) + b[len(v.Pattern)] = 0 + copy(b[len(v.Pattern)+1:], v.Options) +} + +// DecodeRegex decodes [Regex] value from b. +// +// If there is not enough bytes, DecodeRegex will return a wrapped [ErrDecodeShortInput]. +// If the input is otherwise invalid, a wrapped [ErrDecodeInvalidInput] is returned. +func DecodeRegex(b []byte) (Regex, error) { + var res Regex + + if len(b) < 2 { + return res, fmt.Errorf("DecodeRegex: expected at least 2 bytes, got %d: %w", len(b), ErrDecodeShortInput) + } + + p, o := -1, -1 + for i, b := range b { + if b == 0 { + if p == -1 { + p = i + } else { + o = i + break + } + } + } + + if o == -1 { + return res, fmt.Errorf("DecodeRegex: expected two 0 bytes: %w", ErrDecodeShortInput) + } + + res.Pattern = string(b[:p]) + res.Options = string(b[p+1 : o]) + + return res, nil +} diff --git a/bsonproto/string.go b/bsonproto/string.go index 95053ac..519b624 100644 --- a/bsonproto/string.go +++ b/bsonproto/string.go @@ -5,39 +5,44 @@ import ( "fmt" ) -// SizeString returns a size of the encoding of s string in bytes - len(s)+5. -func SizeString(s string) int { - return len(s) + 5 +// SizeString returns a size of the encoding of v string in bytes. +func SizeString(v string) int { + return len(v) + 5 } // EncodeString encodes string value v into b. // -// b must be at least len(v)+5 bytes long; otherwise, EncodeString will panic. +// b must be at least len(v)+5 ([SizeString]) bytes long; otherwise, EncodeString will panic. // Only b[0:len(v)+5] bytes are modified. func EncodeString(b []byte, v string) { - l := len(v) + 5 + i := len(v) + 1 - b[l-1] = 0 - binary.LittleEndian.PutUint32(b, uint32(l)) - copy(b[4:], v) + // ensure b length early + b[4+i-1] = 0 + + binary.LittleEndian.PutUint32(b, uint32(i)) + copy(b[4:4+i-1], v) } // DecodeString decodes string value from b. // -// If there is not enough bytes, DecodeString will return a wrapped ErrDecodeShortInput. -// If the input is otherwise invalid, a wrapped ErrDecodeInvalidInput is returned. +// If there is not enough bytes, DecodeString will return a wrapped [ErrDecodeShortInput]. +// If the input is otherwise invalid, a wrapped [ErrDecodeInvalidInput] is returned. func DecodeString(b []byte) (string, error) { if len(b) < 5 { return "", fmt.Errorf("DecodeString: expected at least 5 bytes, got %d: %w", len(b), ErrDecodeShortInput) } - l := int(binary.LittleEndian.Uint32(b)) - if len(b) < l { - return "", fmt.Errorf("DecodeString: expected at least %d bytes, got %d: %w", l, len(b), ErrDecodeShortInput) + i := int(binary.LittleEndian.Uint32(b)) + if i < 1 { + return "", fmt.Errorf("DecodeString: expected the prefix to be at least 1, got %d: %w", i, ErrDecodeInvalidInput) + } + if e := 4 + i; len(b) < e { + return "", fmt.Errorf("DecodeString: expected at least %d bytes, got %d: %w", e, len(b), ErrDecodeShortInput) } - if b[l-1] != 0 { + if b[4+i-1] != 0 { return "", fmt.Errorf("DecodeString: expected the last byte to be 0: %w", ErrDecodeInvalidInput) } - return string(b[4 : l-1]), nil + return string(b[4 : 4+i-1]), nil } diff --git a/bsonproto/time.go b/bsonproto/time.go index 701fca7..81410c2 100644 --- a/bsonproto/time.go +++ b/bsonproto/time.go @@ -1 +1,33 @@ package bsonproto + +import ( + "encoding/binary" + "fmt" + "time" +) + +// SizeTime is a size of the encoding of [time.Time] in bytes. +const SizeTime = 8 + +// EncodeTime encodes [time.Time] value v into b. +// +// b must be at least 8 ([SizeTime]) byte long; otherwise, EncodeTime will panic. +// Only b[0:8] bytes are modified. +func EncodeTime(b []byte, v time.Time) { + binary.LittleEndian.PutUint64(b, uint64(v.UnixMilli())) +} + +// DecodeTime decodes [time.Time] value from b. +// +// If there is not enough bytes, DecodeTime will return a wrapped [ErrDecodeShortInput]. +func DecodeTime(b []byte) (time.Time, error) { + var res time.Time + + if len(b) < SizeTime { + return res, fmt.Errorf("DecodeTime: expected at least %d bytes, got %d: %w", SizeTime, len(b), ErrDecodeShortInput) + } + + res = time.UnixMilli(int64(binary.LittleEndian.Uint64(b))).UTC() + + return res, nil +} diff --git a/bsonproto/timestamp.go b/bsonproto/timestamp.go index 97e505e..4c01a12 100644 --- a/bsonproto/timestamp.go +++ b/bsonproto/timestamp.go @@ -1,3 +1,31 @@ package bsonproto -type Timestamp struct{} +import ( + "encoding/binary" + "fmt" +) + +// Timestamp represents BSON scalar type timestamp. +type Timestamp uint64 + +// SizeTimestamp is a size of the encoding of [Timestamp] in bytes. +const SizeTimestamp = 8 + +// EncodeTimestamp encodes [Timestamp] value v into b. +// +// b must be at least 8 ([SizeTimestamp]) bytes long; otherwise, EncodeTimestamp will panic. +// Only b[0:8] bytes are modified. +func EncodeTimestamp(b []byte, v Timestamp) { + binary.LittleEndian.PutUint64(b, uint64(v)) +} + +// DecodeTimestamp decodes [Timestamp] value from b. +// +// If there is not enough bytes, DecodeTimestamp will return a wrapped [ErrDecodeShortInput]. +func DecodeTimestamp(b []byte) (Timestamp, error) { + if len(b) < SizeTimestamp { + return 0, fmt.Errorf("DecodeTimestamp: expected at least %d bytes, got %d: %w", SizeTimestamp, len(b), ErrDecodeShortInput) + } + + return Timestamp(binary.LittleEndian.Uint64(b)), nil +}