// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package bsr_test import ( "bytes" "context" "errors" "io" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/hashicorp/boundary/internal/bsr" ) func init() { if err := bsr.RegisterChunkType("TEST", "TEST", func(_ context.Context, bc *bsr.BaseChunk, data []byte) (bsr.Chunk, error) { return &testChunk{ BaseChunk: bc, Data: data, }, nil }); err != nil { panic(err) } if err := bsr.RegisterChunkType("TEST", "ERRR", func(_ context.Context, bc *bsr.BaseChunk, data []byte) (bsr.Chunk, error) { return nil, errors.New("error in decode function") }); err != nil { panic(err) } } func TestChunkDecoder(t *testing.T) { ctx := context.Background() ts := time.Date(2023, time.March, 16, 10, 47, 3, 14, time.UTC) cases := []struct { name string encoded []byte want []bsr.Chunk }{ { "header-no-compression", []byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "sess_123456789" + // data "\xbe\x4c\x7c\x20" + // crc "", ), []bsr.Chunk{ &bsr.HeaderChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts), Type: bsr.ChunkHeader, }, Compression: bsr.NoCompression, Encryption: bsr.NoEncryption, SessionId: "sess_123456789", }, }, }, { "header-end-no-compression", []byte( "" + // header "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "sess_123456789" + // data "\xbe\x4c\x7c\x20" + // crc "" + // end "\x00\x00\x00\x00" + // length "TEST" + // protocol "DONE" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x13" + // time nanoseconds "\x50\x91\xfe\x72" + // crc "", ), []bsr.Chunk{ &bsr.HeaderChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts), Type: bsr.ChunkHeader, }, Compression: bsr.NoCompression, Encryption: bsr.NoEncryption, SessionId: "sess_123456789", }, &bsr.EndChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts.Add(time.Nanosecond * 5)), Type: bsr.ChunkEnd, }, }, }, }, { "header-test-end-no-compression", []byte( "" + // header "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "sess_123456789" + // data "\xbe\x4c\x7c\x20" + // crc "" + // test "\x00\x00\x00\x03" + // length "TEST" + // protocol "TEST" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "foo" + // data "\xa4\x6e\x48\x70" + // crc "" + // end "\x00\x00\x00\x00" + // length "TEST" + // protocol "DONE" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x13" + // time nanoseconds "\x50\x91\xfe\x72" + // crc "", ), []bsr.Chunk{ &bsr.HeaderChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts), Type: bsr.ChunkHeader, }, Compression: bsr.NoCompression, Encryption: bsr.NoEncryption, SessionId: "sess_123456789", }, &testChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts), Type: "TEST", }, Data: []byte("foo"), }, &bsr.EndChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts.Add(time.Nanosecond * 5)), Type: bsr.ChunkEnd, }, }, }, }, { "header-test-end-gzip-compression", []byte( "" + // header "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x01" + // compression method "\x00" + // encryption method "sess_123456789" + // data "\x10\x24\xed\xb1" + // crc "" + // test "\x00\x00\x00\x1b" + // length "TEST" + // protocol "TEST" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds gziped("foo") + // data "\x29\x8e\x22\x12" + // crc "" + // end "\x00\x00\x00\x00" + // length "TEST" + // protocol "DONE" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x13" + // time nanoseconds "\x50\x91\xfe\x72" + // crc "", ), []bsr.Chunk{ &bsr.HeaderChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts), Type: bsr.ChunkHeader, }, Compression: bsr.GzipCompression, Encryption: bsr.NoEncryption, SessionId: "sess_123456789", }, &testChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts), Type: "TEST", }, Data: []byte("foo"), }, &bsr.EndChunk{ BaseChunk: &bsr.BaseChunk{ Protocol: "TEST", Direction: bsr.Inbound, Timestamp: bsr.NewTimestamp(ts.Add(time.Nanosecond * 5)), Type: bsr.ChunkEnd, }, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { buf := bytes.NewBuffer(tc.encoded) dec, err := bsr.NewChunkDecoder(ctx, buf) require.NoError(t, err) got := make([]bsr.Chunk, 0, len(tc.want)) for { c, err := dec.Decode(ctx) if err == io.EOF { break } require.NoError(t, err) got = append(got, c) } assert.Equal(t, tc.want, got) }) } } func TestNewChunkDecoderErrors(t *testing.T) { ctx := context.Background() cases := []struct { name string r io.Reader want error }{ { "nil-reader", nil, errors.New("bsr.NewChunkDecoder: reader cannot be nil: invalid parameter"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := bsr.NewChunkDecoder(ctx, tc.r) assert.EqualError(t, err, tc.want.Error()) }) } } func TestChunkDecoderDecodeErrors(t *testing.T) { ctx := context.Background() cases := []struct { name string r io.Reader want error }{ { "unknown-chunk-type", bytes.NewBuffer([]byte( "" + // test "\x00\x00\x00\x03" + // length "TEST" + // protocol "UNKW" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "foo" + // data "\x01\x96\x41\x8a" + // crc "", )), errors.New("bsr.(ChunkDecoder).Decode: unsupported chunk type UNKW for protocol TEST: error decoding chunk"), }, { "unknown-chunk-type-for-protocol", bytes.NewBuffer([]byte( "" + // test "\x00\x00\x00\x03" + // length "UNKW" + // protocol "TEST" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "foo" + // data "\x38\xbd\xb9\xee" + // crc "", )), errors.New("bsr.(ChunkDecoder).Decode: unsupported chunk type TEST for protocol UNKW: error decoding chunk"), }, { "chuck-missing-base-fields", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "", )), errors.New("bsr.(ChunkDecoder).Decode: unexpected EOF: error decoding chunk"), }, { "chuck-length-exceeds-max", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\xff\xff\xff\xff" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "", )), errors.New("bsr.(ChunkDecoder).Decode: chunk length 4294967295 exceeds max chunk length of 6400000: error decoding chunk"), }, { "chuck-missing-data", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "", )), errors.New("bsr.(ChunkDecoder).Decode: unexpected EOF: missing data: error decoding chunk"), }, { "chuck-partial-missing-data", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "", )), errors.New("bsr.(ChunkDecoder).Decode: unexpected EOF: missing data: error decoding chunk"), }, { "chuck-missing-crc", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "sess_123456789" + // data "", )), errors.New("bsr.(ChunkDecoder).Decode: unexpected EOF: missing crc: error decoding chunk"), }, { "chuck-partial-missing-crc", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "sess_123456789" + // data "\xbe\x4c\x7c" + // crc "", )), errors.New("bsr.(ChunkDecoder).Decode: unexpected EOF: missing crc: error decoding chunk"), }, { "chuck-crc-mismatch", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "sess_123456789" + // data "\xbe\x4c\x7c\x00" + // crc "", )), errors.New("bsr.(ChunkDecoder).Decode: chunk crc did not match: error decoding chunk"), }, { "chuck-invalid-direction", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x10" + // length "TEST" + // protocol "HEAD" + // type "\x00" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "\x00" + // compression method "\x00" + // encryption method "sess_123456789" + // data "\xdd\x4b\xa5\x04" + // crc "", )), errors.New("bsr.(ChunkDecoder).Decode: bsr.NewBaseChunk: invalid direction: invalid parameter: error decoding chunk"), }, { "chuck-decode-function-error", bytes.NewBuffer([]byte( "" + // so everything else aligns better "\x00\x00\x00\x03" + // length "TEST" + // protocol "ERRR" + // type "\x01" + // direction "\x00\x00\x00\x00\x64\x12\xf3\xa7" + // time seconds "\x00\x00\x00\x0e" + // time nanoseconds "foo" + // data "\x30\xd5\x69\xbb" + // crc "", )), errors.New("bsr.(ChunkDecoder).Decode: error in decode function: error decoding chunk"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { dec, err := bsr.NewChunkDecoder(ctx, tc.r) require.NoError(t, err) _, err = dec.Decode(ctx) assert.EqualError(t, err, tc.want.Error()) }) } }