// Copyright (C) MongoDB, Inc. 2024-present. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 package bson import ( "bytes" "encoding/binary" "errors" "io" "testing" "gitea.psichedelico.com/go/bson/internal/assert" "gitea.psichedelico.com/go/bson/internal/require" "gitea.psichedelico.com/go/bson/x/bsonx/bsoncore" "github.com/google/go-cmp/cmp" ) func TestReadArray(t *testing.T) { t.Parallel() testCases := []struct { name string ioReader io.Reader arr RawArray err error }{ { "nil reader", nil, nil, ErrNilReader, }, { "premature end of reader", bytes.NewBuffer([]byte{}), nil, io.EOF, }, { "empty Array", bytes.NewBuffer([]byte{5, 0, 0, 0, 0}), []byte{5, 0, 0, 0, 0}, nil, }, { "non-empty Array", bytes.NewBuffer([]byte{ '\x1B', '\x00', '\x00', '\x00', '\x02', '0', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x72', '\x00', '\x02', '1', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x7a', '\x00', '\x00', }), []byte{ '\x1B', '\x00', '\x00', '\x00', '\x02', '0', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x72', '\x00', '\x02', '1', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x7a', '\x00', '\x00', }, nil, }, } for _, tc := range testCases { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() arr, err := ReadArray(tc.ioReader) if !assert.CompareErrors(err, tc.err) { t.Errorf("errors do not match. got %v; want %v", err, tc.err) } if !bytes.Equal(tc.arr, arr) { t.Errorf("Arrays differ. got %v; want %v", tc.arr, arr) } }) } } func TestArray_Validate(t *testing.T) { t.Parallel() t.Run("TooShort", func(t *testing.T) { t.Parallel() want := bsoncore.NewInsufficientBytesError(nil, nil) got := RawArray{'\x00', '\x00'}.Validate() if !assert.CompareErrors(got, want) { t.Errorf("Did not get expected error. got %v; want %v", got, want) } }) t.Run("InvalidLength", func(t *testing.T) { t.Parallel() want := bsoncore.NewArrayLengthError(200, 5) r := make(RawArray, 5) binary.LittleEndian.PutUint32(r[0:4], 200) got := r.Validate() if !assert.CompareErrors(got, want) { t.Errorf("Did not get expected error. got %v; want %v", got, want) } }) t.Run("Invalid Element", func(t *testing.T) { t.Parallel() want := bsoncore.NewInsufficientBytesError(nil, nil) r := make(RawArray, 7) binary.LittleEndian.PutUint32(r[0:4], 7) r[4], r[5], r[6] = 0x02, 'f', 0x00 got := r.Validate() if !assert.CompareErrors(got, want) { t.Errorf("Did not get expected error. got %v; want %v", got, want) } }) t.Run("Missing Null Terminator", func(t *testing.T) { t.Parallel() want := bsoncore.ErrMissingNull r := make(RawArray, 6) binary.LittleEndian.PutUint32(r[0:4], 6) r[4], r[5] = 0x0A, '0' got := r.Validate() if !assert.CompareErrors(got, want) { t.Errorf("Did not get expected error. got %v; want %v", got, want) } }) testCases := []struct { name string r RawArray want error }{ {"array null", RawArray{'\x08', '\x00', '\x00', '\x00', '\x0A', '0', '\x00', '\x00'}, nil}, {"array", RawArray{ '\x1B', '\x00', '\x00', '\x00', '\x02', '0', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x72', '\x00', '\x02', '1', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x7a', '\x00', '\x00', }, nil, }, {"subarray", RawArray{ '\x13', '\x00', '\x00', '\x00', '\x04', '0', '\x00', '\x0B', '\x00', '\x00', '\x00', '\x0A', '0', '\x00', '\x0A', '1', '\x00', '\x00', '\x00', }, nil, }, {"invalid key order", RawArray{ '\x0B', '\x00', '\x00', '\x00', '\x0A', '2', '\x00', '\x0A', '0', '\x00', '\x00', '\x00', }, errors.New(`array key "2" is out of order or invalid`), }, {"invalid key type", RawArray{ '\x0B', '\x00', '\x00', '\x00', '\x0A', 'p', '\x00', '\x0A', 'q', '\x00', '\x00', '\x00', }, errors.New(`array key "p" is out of order or invalid`), }, } for _, tc := range testCases { tc := tc // Capture the range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() got := tc.r.Validate() if !assert.CompareErrors(got, tc.want) { t.Errorf("Returned error does not match. got %v; want %v", got, tc.want) } }) } } func TestArray_Index(t *testing.T) { t.Parallel() t.Run("Out of bounds", func(t *testing.T) { t.Parallel() rdr := RawArray{0xe, 0x0, 0x0, 0x0, 0xa, '0', 0x0, 0xa, '1', 0x0, 0xa, 0x7a, 0x0, 0x0} _, err := rdr.IndexErr(3) if !errors.Is(err, bsoncore.ErrOutOfBounds) { t.Errorf("Out of bounds should be returned when accessing element beyond end of Array. got %v; want %v", err, bsoncore.ErrOutOfBounds) } }) t.Run("Validation Error", func(t *testing.T) { t.Parallel() rdr := RawArray{0x07, 0x00, 0x00, 0x00, 0x00} _, got := rdr.IndexErr(1) want := bsoncore.NewInsufficientBytesError(nil, nil) if !assert.CompareErrors(got, want) { t.Errorf("Did not receive expected error. got %v; want %v", got, want) } }) testArray := RawArray{ '\x26', '\x00', '\x00', '\x00', '\x02', '0', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x72', '\x00', '\x02', '1', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x7a', '\x00', '\x02', '2', '\x00', '\x04', '\x00', '\x00', '\x00', '\x71', '\x75', '\x78', '\x00', '\x00', } testCases := []struct { name string index uint want RawValue }{ {"first", 0, RawValue{ Type: TypeString, Value: []byte{0x04, 0x00, 0x00, 0x00, 0x62, 0x61, 0x72, 0x00}, }, }, {"second", 1, RawValue{ Type: TypeString, Value: []byte{0x04, 0x00, 0x00, 0x00, 0x62, 0x61, 0x7a, 0x00}, }, }, {"third", 2, RawValue{ Type: TypeString, Value: []byte{0x04, 0x00, 0x00, 0x00, 0x71, 0x75, 0x78, 0x00}, }, }, } for _, tc := range testCases { tc := tc // Capture the range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() t.Run("IndexErr", func(t *testing.T) { t.Parallel() got, err := testArray.IndexErr(tc.index) if err != nil { t.Errorf("Unexpected error from IndexErr: %s", err) } if diff := cmp.Diff(got, tc.want); diff != "" { t.Errorf("Arrays differ: (-got +want)\n%s", diff) } }) t.Run("Index", func(t *testing.T) { t.Parallel() defer func() { if err := recover(); err != nil { t.Errorf("Unexpected error: %v", err) } }() got := testArray.Index(tc.index) if diff := cmp.Diff(got, tc.want); diff != "" { t.Errorf("Arrays differ: (-got +want)\n%s", diff) } }) }) } } func TestRawArray_Stringt(t *testing.T) { t.Parallel() testCases := []struct { name string arr RawArray arrayString string arrayDebugString string }{ { "array", RawArray{ '\x1B', '\x00', '\x00', '\x00', '\x02', '0', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x72', '\x00', '\x02', '1', '\x00', '\x04', '\x00', '\x00', '\x00', '\x62', '\x61', '\x7a', '\x00', '\x00', }, `["bar","baz"]`, `Array(27)["bar","baz"]`, }, { "subarray", RawArray{ '\x13', '\x00', '\x00', '\x00', '\x04', '0', '\x00', '\x0B', '\x00', '\x00', '\x00', '\x0A', '0', '\x00', '\x0A', '1', '\x00', '\x00', '\x00', }, `[[null,null]]`, `Array(19)[Array(11)[null,null]]`, }, { "malformed--length too small", RawArray{ '\x04', '\x00', '\x00', '\x00', '\x00', }, ``, `Array(4)[]`, }, { "malformed--length too large", RawArray{ '\x13', '\x00', '\x00', '\x00', '\x00', }, ``, `Array(19)[]`, }, { "malformed--missing null byte", RawArray{ '\x06', '\x00', '\x00', '\x00', '\x02', '0', }, ``, `Array(6)[]`, }, } for _, tc := range testCases { tc := tc // Capture the range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() arrayString := tc.arr.String() if arrayString != tc.arrayString { t.Errorf("array strings do not match. got %q; want %q", arrayString, tc.arrayString) } arrayDebugString := tc.arr.DebugString() if arrayDebugString != tc.arrayDebugString { t.Errorf("array debug strings do not match. got %q; want %q", arrayDebugString, tc.arrayDebugString) } }) } } func TestRawArray_Values(t *testing.T) { t.Parallel() testCases := []struct { name string arr RawArray want []RawValue }{ { name: "empty", arr: []byte{0x05, 0x00, 0x00, 0x00, 0x00}, want: []RawValue{}, }, { name: "null document", arr: []byte{0x07, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00}, // [{}] want: []RawValue{ { Type: TypeNull, Value: []byte{}, }, }, }, { name: "same", arr: []byte{0x13, 0x00, 0x00, 0x00, 0x10, 0x30, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x31, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00}, // [int32(1), int32(2)] want: []RawValue{ { // int32(1) Type: TypeInt32, Value: []byte{0x01, 0x00, 0x00, 0x00}, }, { // int32(2) Type: TypeInt32, Value: []byte{0x02, 0x00, 0x00, 0x00}, }, }, }, { name: "mixed", arr: []byte{0x17, 0x00, 0x00, 0x00, 0x10, 0x30, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x31, 0x00, 0x04, 0x00, 0x00, 0x00, 0x66, 0x6F, 0x6F, 0x00, 0x00}, // [int32(1), "foo"] want: []RawValue{ { // int32(1) Type: TypeInt32, Value: []byte{0x01, 0x00, 0x00, 0x00}, }, { // "foo" Type: TypeString, Value: []byte{0x04, 0x00, 0x00, 0x00, 0x66, 0x6F, 0x6F, 0x00}, }, }, }, } for _, tcase := range testCases { tcase := tcase t.Run(tcase.name, func(t *testing.T) { t.Parallel() values, err := tcase.arr.Values() require.NoError(t, err, "failed to turn array into values") require.Len(t, values, len(tcase.want), "got len does not match want") for idx, want := range tcase.want { assert.True(t, want.Equal(values[idx]), "want: %v, got: %v", want, values[idx]) } }) } }