diff --git a/internal/bsr/chunk.go b/internal/bsr/chunk.go new file mode 100644 index 0000000000..c881cf6005 --- /dev/null +++ b/internal/bsr/chunk.go @@ -0,0 +1,111 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +import ( + "context" + + "github.com/hashicorp/boundary/internal/errors" +) + +// sizes +const ( + lengthSize = 4 + protocolSize = 4 + chunkTypeSize = 4 + directionSize = 1 + + chunkBaseSize = lengthSize + protocolSize + chunkTypeSize + directionSize + timestampSize + crcSize = 4 +) + +// Chunk Types +const ( + ChunkHeader ChunkType = "HEAD" + ChunkEnd ChunkType = "DONE" +) + +// ChunkType identifies the type of a chunk. +type ChunkType string + +// ValidChunkType checks ifa given ChunkType is valid. +func ValidChunkType(c ChunkType) bool { + return len(c) <= chunkTypeSize +} + +// Chunk is a section of a bsr data file. +type Chunk interface { + // GetLength returns the length of the chunk data. + GetLength() uint32 + // GetProtocol returns the protocol of the recorded data. + GetProtocol() Protocol + // GetType returns the chunk type. + GetType() ChunkType + // GetDirection returns the direction of the data in the chunk. + GetDirection() Direction + // GetTimestamp returns the timestamp of a Chunk. + GetTimestamp() *Timestamp + + // MarshalData serializes the data portion of a chunk. + MarshalData(context.Context) ([]byte, error) +} + +// BaseChunk contains the common fields of all chunk types. +type BaseChunk struct { + Protocol Protocol + Direction Direction + Timestamp *Timestamp + Type ChunkType + + length uint32 +} + +// NewBaseChunk creates a BaseChunk. +func NewBaseChunk(ctx context.Context, p Protocol, d Direction, t *Timestamp, typ ChunkType) (*BaseChunk, error) { + const op = "bsr.NewBaseChunk" + if !ValidProtocol(p) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "protocol name cannot be greater than 4 characters") + } + if !ValidDirection(d) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid direction") + } + if t == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "timestamp must not be nil") + } + if !ValidChunkType(typ) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "chunk type cannot be greater than 4 characters") + } + + return &BaseChunk{ + Protocol: p, + Direction: d, + Timestamp: t, + Type: typ, + }, nil +} + +// GetLength returns the length of the chunk data. +func (b *BaseChunk) GetLength() uint32 { + return b.length +} + +// GetProtocol returns the protocol of the recorded data. +func (b *BaseChunk) GetProtocol() Protocol { + return b.Protocol +} + +// GetType returns the chunk type. +func (b *BaseChunk) GetType() ChunkType { + return b.Type +} + +// GetDirection returns the direction of the data in the chunk. +func (b *BaseChunk) GetDirection() Direction { + return b.Direction +} + +// GetTimestamp returns the timestamp of a Chunk. +func (b *BaseChunk) GetTimestamp() *Timestamp { + return b.Timestamp +} diff --git a/internal/bsr/chunk_end.go b/internal/bsr/chunk_end.go new file mode 100644 index 0000000000..a7b9fed6ea --- /dev/null +++ b/internal/bsr/chunk_end.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +import "context" + +// EndChunk identifies the end of the chunks in a BSR data file. +// An EndChunk in a bsr data file is represented as: +// +// uint32 length 4 bytes +// uint32 protocol 4 bytes +// uint32 chunk_type 4 bytes +// uint8 direction 1 byte +// timest timestamp 12 bytes +// data 0 bytes +// uint32 crc 4 bytes +type EndChunk struct { + *BaseChunk +} + +// MarshalData returns an empty byte slice. +func (c *EndChunk) MarshalData(_ context.Context) ([]byte, error) { + return nil, nil +} + +// NewEnd creates an EndChunk. +func NewEnd(ctx context.Context, p Protocol, d Direction, t *Timestamp) (*EndChunk, error) { + const op = "bsr.NewHeader" + + bc, err := NewBaseChunk(ctx, p, d, t, ChunkEnd) + if err != nil { + return nil, err + } + + return &EndChunk{ + BaseChunk: bc, + }, nil +} diff --git a/internal/bsr/chunk_end_test.go b/internal/bsr/chunk_end_test.go new file mode 100644 index 0000000000..69b4582e75 --- /dev/null +++ b/internal/bsr/chunk_end_test.go @@ -0,0 +1,128 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewEndChunk(t *testing.T) { + ctx := context.Background() + now := time.Now() + + cases := []struct { + name string + p bsr.Protocol + d bsr.Direction + t *bsr.Timestamp + want *bsr.EndChunk + wantErr error + }{ + { + "valid-nocompression-noencrpytion", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + &bsr.EndChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkEnd, + }, + }, + nil, + }, + { + "valid-gzip-noencrpytion", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + &bsr.EndChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkEnd, + }, + }, + nil, + }, + { + "invalid-protocol", + bsr.Protocol("TEST_INVALID"), + bsr.Inbound, + bsr.NewTimestamp(now), + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "protocol name cannot be greater than 4 characters"), + }, + { + "invalid-direction", + bsr.Protocol("TEST"), + bsr.UnknownDirection, + bsr.NewTimestamp(now), + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "invalid direction"), + }, + { + "invalid-timestamp", + bsr.Protocol("TEST"), + bsr.Inbound, + nil, + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "timestamp must not be nil"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := bsr.NewEnd(ctx, tc.p, tc.d, tc.t) + if tc.wantErr != nil { + assert.EqualError(t, tc.wantErr, err.Error()) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestEndMarshalData(t *testing.T) { + ctx := context.Background() + now := time.Now() + + cases := []struct { + name string + h *bsr.EndChunk + want []byte + }{ + { + "nocompression-noencrpytion", + &bsr.EndChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkEnd, + }, + }, + nil, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.h.MarshalData(ctx) + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/bsr/chunk_header.go b/internal/bsr/chunk_header.go new file mode 100644 index 0000000000..0b4be332f6 --- /dev/null +++ b/internal/bsr/chunk_header.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +import ( + "context" + + "github.com/hashicorp/boundary/internal/errors" +) + +// HeaderChunk is the first chunk in a BSR data file. +// A HeaderChunk in a bsr data file is represented as: +// +// uint32 length 4 bytes +// uint32 protocol 4 bytes +// uint32 chunk_type 4 bytes +// uint8 direction 1 byte +// timest timestamp 12 bytes +// uint8 compression 1 byte +// uint8 encryption 1 byte +// session_id variable +// uint32 crc 4 bytes +type HeaderChunk struct { + *BaseChunk + Compression Compression + Encryption Encryption + SessionId string +} + +// MarshalData serializes a HeaderChunk. +func (h *HeaderChunk) MarshalData(_ context.Context) ([]byte, error) { + b := make([]byte, 0, len(h.SessionId)+compressionSize+encryptionSize) + b = append(b, byte(h.Compression)) + b = append(b, byte(h.Encryption)) + b = append(b, []byte(h.SessionId)...) + return b, nil +} + +// NewHeader creates a HeaderChunk. +func NewHeader(ctx context.Context, p Protocol, d Direction, t *Timestamp, c Compression, e Encryption, sessionId string) (*HeaderChunk, error) { + const op = "bsr.NewHeader" + + bc, err := NewBaseChunk(ctx, p, d, t, ChunkHeader) + if err != nil { + return nil, err + } + + if !ValidCompression(c) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid compression") + } + + if !ValidEncryption(e) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid encryption") + } + + return &HeaderChunk{ + BaseChunk: bc, + Compression: c, + Encryption: e, + SessionId: sessionId, + }, nil +} diff --git a/internal/bsr/chunk_header_test.go b/internal/bsr/chunk_header_test.go new file mode 100644 index 0000000000..2fd865e56c --- /dev/null +++ b/internal/bsr/chunk_header_test.go @@ -0,0 +1,192 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewHeaderChunk(t *testing.T) { + ctx := context.Background() + now := time.Now() + + cases := []struct { + name string + p bsr.Protocol + d bsr.Direction + t *bsr.Timestamp + c bsr.Compression + e bsr.Encryption + sessionId string + want *bsr.HeaderChunk + wantErr error + }{ + { + "valid-nocompression-noencrpytion", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.NoCompression, + bsr.NoEncryption, + "sess_123456789", + &bsr.HeaderChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkHeader, + }, + Compression: bsr.NoCompression, + Encryption: bsr.NoEncryption, + SessionId: "sess_123456789", + }, + nil, + }, + { + "valid-gzip-noencrpytion", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.GzipCompression, + bsr.NoEncryption, + "sess_123456789", + &bsr.HeaderChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkHeader, + }, + Compression: bsr.GzipCompression, + Encryption: bsr.NoEncryption, + SessionId: "sess_123456789", + }, + nil, + }, + { + "invalid-protocol", + bsr.Protocol("TEST_INVALID"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.NoCompression, + bsr.NoEncryption, + "sess_123456789", + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "protocol name cannot be greater than 4 characters"), + }, + { + "invalid-direction", + bsr.Protocol("TEST"), + bsr.UnknownDirection, + bsr.NewTimestamp(now), + bsr.NoCompression, + bsr.NoEncryption, + "sess_123456789", + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "invalid direction"), + }, + { + "invalid-timestamp", + bsr.Protocol("TEST"), + bsr.Inbound, + nil, + bsr.NoCompression, + bsr.NoEncryption, + "sess_123456789", + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "timestamp must not be nil"), + }, + { + "invalid-compression", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.Compression(255), + bsr.NoEncryption, + "sess_123456789", + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewHeader", "invalid compression"), + }, + { + "invalid-encryption", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.NoCompression, + bsr.Encryption(255), + "sess_123456789", + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewHeader", "invalid encryption"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := bsr.NewHeader(ctx, tc.p, tc.d, tc.t, tc.c, tc.e, tc.sessionId) + if tc.wantErr != nil { + assert.EqualError(t, tc.wantErr, err.Error()) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestHeaderMarshalData(t *testing.T) { + ctx := context.Background() + now := time.Now() + + cases := []struct { + name string + h *bsr.HeaderChunk + want []byte + }{ + { + "nocompression-noencrpytion", + &bsr.HeaderChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkHeader, + }, + Compression: bsr.NoCompression, + Encryption: bsr.NoEncryption, + SessionId: "sess_123456789", + }, + []byte("\x00\x00sess_123456789"), + }, + { + "gzip-noencrpytion", + &bsr.HeaderChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkHeader, + }, + Compression: bsr.GzipCompression, + Encryption: bsr.NoEncryption, + SessionId: "sess_123456789", + }, + []byte("\x01\x00sess_123456789"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.h.MarshalData(ctx) + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/bsr/chunk_test.go b/internal/bsr/chunk_test.go new file mode 100644 index 0000000000..4e80346ef6 --- /dev/null +++ b/internal/bsr/chunk_test.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBaseChunk(t *testing.T) { + ctx := context.Background() + now := time.Now() + + cases := []struct { + name string + p bsr.Protocol + d bsr.Direction + t *bsr.Timestamp + typ bsr.ChunkType + want *bsr.BaseChunk + wantErr error + }{ + { + "valid", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.ChunkType("TEST"), + &bsr.BaseChunk{ + Protocol: bsr.Protocol("TEST"), + Direction: bsr.Inbound, + Timestamp: bsr.NewTimestamp(now), + Type: bsr.ChunkType("TEST"), + }, + nil, + }, + { + "invalid-protocol", + bsr.Protocol("TEST_INVALID"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.ChunkType("TEST"), + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "protocol name cannot be greater than 4 characters"), + }, + { + "invalid-direction", + bsr.Protocol("TEST"), + bsr.UnknownDirection, + bsr.NewTimestamp(now), + bsr.ChunkType("TEST"), + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "invalid direction"), + }, + { + "invalid-timestamp", + bsr.Protocol("TEST"), + bsr.Inbound, + nil, + bsr.ChunkType("TEST"), + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "timestamp must not be nil"), + }, + { + "invalid-chunk-type", + bsr.Protocol("TEST"), + bsr.Inbound, + bsr.NewTimestamp(now), + bsr.ChunkType("TEST_INVALID"), + nil, + errors.New(ctx, errors.InvalidParameter, "bsr.NewBaseChunk", "chunk type cannot be greater than 4 characters"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := bsr.NewBaseChunk(ctx, tc.p, tc.d, tc.t, tc.typ) + if tc.wantErr != nil { + assert.EqualError(t, tc.wantErr, err.Error()) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.want, got) + + assert.Equal(t, tc.want.Protocol, got.GetProtocol()) + assert.Equal(t, tc.want.Direction, got.GetDirection()) + assert.Equal(t, tc.want.Timestamp, got.GetTimestamp()) + assert.Equal(t, tc.want.Type, got.GetType()) + assert.Equal(t, tc.want.GetLength(), got.GetLength()) + }) + } +} diff --git a/internal/bsr/compression.go b/internal/bsr/compression.go new file mode 100644 index 0000000000..6e730c8bab --- /dev/null +++ b/internal/bsr/compression.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +import ( + "bytes" + "io" +) + +const ( + compressionSize = 1 +) + +// Compression is used to identify the compression used for the data in chunks. +type Compression uint8 + +// Supported compression methods. +const ( + NoCompression Compression = iota + GzipCompression +) + +func (c Compression) String() string { + switch c { + case NoCompression: + return "no compression" + case GzipCompression: + return "gzip" + default: + return "unknown compression" + } +} + +// ValidCompression checks if a given Compression is valid. +func ValidCompression(c Compression) bool { + switch c { + case NoCompression, GzipCompression: + return true + } + return false +} + +type nullCompressionWriter struct { + *bytes.Buffer +} + +func (w *nullCompressionWriter) Close() error { + return nil +} + +func newNullCompressionWriter(b *bytes.Buffer) io.WriteCloser { + return &nullCompressionWriter{Buffer: b} +} diff --git a/internal/bsr/compression_null_compression_test.go b/internal/bsr/compression_null_compression_test.go new file mode 100644 index 0000000000..2a0f4b6fc7 --- /dev/null +++ b/internal/bsr/compression_null_compression_test.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNullCompressionWriter(t *testing.T) { + var buf bytes.Buffer + var compressor io.WriteCloser + + expect := []byte("uncompressed data") + compressor = newNullCompressionWriter(&buf) + + wrote, err := compressor.Write(expect) + require.NoError(t, err) + assert.Equal(t, len(expect), wrote) + + err = compressor.Close() + require.NoError(t, err) + + assert.Equal(t, expect, buf.Bytes()) +} diff --git a/internal/bsr/compression_test.go b/internal/bsr/compression_test.go new file mode 100644 index 0000000000..f8d114e059 --- /dev/null +++ b/internal/bsr/compression_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/stretchr/testify/assert" +) + +func TestValidCompression(t *testing.T) { + cases := []struct { + name string + in bsr.Compression + want bool + }{ + { + bsr.NoCompression.String(), + bsr.NoCompression, + true, + }, + { + bsr.GzipCompression.String(), + bsr.GzipCompression, + true, + }, + { + "something else", + bsr.Compression(255), + false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := bsr.ValidCompression(tc.in) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCompressionString(t *testing.T) { + cases := []struct { + name string + in bsr.Compression + want string + }{ + { + bsr.NoCompression.String(), + bsr.NoCompression, + "no compression", + }, + { + bsr.GzipCompression.String(), + bsr.GzipCompression, + "gzip", + }, + { + "something else", + bsr.Compression(255), + "unknown compression", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.in.String() + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/bsr/direction.go b/internal/bsr/direction.go new file mode 100644 index 0000000000..d4d0e79057 --- /dev/null +++ b/internal/bsr/direction.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +// Direction identifies the directionality of the data captured +// in the chunk. +type Direction uint8 + +// Directions +const ( + UnknownDirection Direction = iota + Inbound + Outbound +) + +func (d Direction) String() string { + switch d { + case Inbound: + return "inbound" + case Outbound: + return "outbound" + default: + return "unknown direction" + } +} + +// ValidDirection checks if a given Direction is valid. +func ValidDirection(d Direction) bool { + switch d { + case Inbound, Outbound: + return true + } + return false +} diff --git a/internal/bsr/direction_test.go b/internal/bsr/direction_test.go new file mode 100644 index 0000000000..f94514041f --- /dev/null +++ b/internal/bsr/direction_test.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/stretchr/testify/assert" +) + +func TestValidDirection(t *testing.T) { + cases := []struct { + name string + in bsr.Direction + want bool + }{ + { + bsr.Inbound.String(), + bsr.Inbound, + true, + }, + { + bsr.Outbound.String(), + bsr.Outbound, + true, + }, + { + bsr.UnknownDirection.String(), + bsr.UnknownDirection, + false, + }, + { + "something else", + bsr.Direction(255), + false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := bsr.ValidDirection(tc.in) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestDirectionString(t *testing.T) { + cases := []struct { + name string + in bsr.Direction + want string + }{ + { + bsr.Inbound.String(), + bsr.Inbound, + "inbound", + }, + { + bsr.Outbound.String(), + bsr.Outbound, + "outbound", + }, + { + bsr.UnknownDirection.String(), + bsr.UnknownDirection, + "unknown direction", + }, + { + "something else", + bsr.Direction(255), + "unknown direction", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.in.String() + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/bsr/doc.go b/internal/bsr/doc.go new file mode 100644 index 0000000000..937f2e3619 --- /dev/null +++ b/internal/bsr/doc.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +/* +Package bsr is used to read and write boundary session recordings. +*/ +package bsr diff --git a/internal/bsr/encryption.go b/internal/bsr/encryption.go new file mode 100644 index 0000000000..f9261a1bba --- /dev/null +++ b/internal/bsr/encryption.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +const ( + encryptionSize = 1 +) + +// Encryption is used to identify the encryption used for the data in chunks. +type Encryption uint8 + +// Supported encryption methods. +const ( + NoEncryption Encryption = iota +) + +func (e Encryption) String() string { + switch e { + case NoEncryption: + return "no encryption" + default: + return "unknown encryption" + } +} + +// ValidEncryption checks if a given Encryption is valid. +func ValidEncryption(e Encryption) bool { + switch e { + case NoEncryption: + return true + } + return false +} diff --git a/internal/bsr/encryption_test.go b/internal/bsr/encryption_test.go new file mode 100644 index 0000000000..9f45a755f3 --- /dev/null +++ b/internal/bsr/encryption_test.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/stretchr/testify/assert" +) + +func TestValidEncrpytion(t *testing.T) { + cases := []struct { + name string + in bsr.Encryption + want bool + }{ + { + bsr.NoEncryption.String(), + bsr.NoEncryption, + true, + }, + { + "something else", + bsr.Encryption(255), + false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := bsr.ValidEncryption(tc.in) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestEncryptionString(t *testing.T) { + cases := []struct { + name string + in bsr.Encryption + want string + }{ + { + bsr.NoEncryption.String(), + bsr.NoEncryption, + "no encryption", + }, + { + "something else", + bsr.Encryption(255), + "unknown encryption", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.in.String() + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/bsr/magic.go b/internal/bsr/magic.go new file mode 100644 index 0000000000..e828b7ef21 --- /dev/null +++ b/internal/bsr/magic.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +const ( + // Magic is the magic string / magic number / file signature used to + // identify a BSR data file. + // + // See: https://en.wikipedia.org/wiki/File_format#Magic_number + Magic magic = magic("\x89BSR\r\n\x1a\n") + + magicSize = len(Magic) +) + +type magic string + +// Bytes returns the magic as a []byte. +func (s magic) Bytes() []byte { + return []byte(s) +} diff --git a/internal/bsr/magic_test.go b/internal/bsr/magic_test.go new file mode 100644 index 0000000000..5176f2557d --- /dev/null +++ b/internal/bsr/magic_test.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/stretchr/testify/assert" +) + +func TestMagic(t *testing.T) { + assert.Equal(t, string(bsr.Magic), "\x89BSR\r\n\x1a\n") + assert.Equal(t, bsr.Magic.Bytes(), []byte("\x89BSR\r\n\x1a\n")) +} diff --git a/internal/bsr/protocol.go b/internal/bsr/protocol.go new file mode 100644 index 0000000000..e4ad1d8954 --- /dev/null +++ b/internal/bsr/protocol.go @@ -0,0 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +// Protocol identifies the protocol of the data captured in a chunk. +type Protocol string + +// ValidProtocol checks if a given Protocol is valid. +func ValidProtocol(p Protocol) bool { + return len(p) <= protocolSize +} diff --git a/internal/bsr/protocol_test.go b/internal/bsr/protocol_test.go new file mode 100644 index 0000000000..e72886de11 --- /dev/null +++ b/internal/bsr/protocol_test.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr_test + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/stretchr/testify/assert" +) + +func TestValidProtocol(t *testing.T) { + cases := []struct { + name string + in bsr.Protocol + want bool + }{ + { + "Valid", + bsr.Protocol("VALI"), + true, + }, + { + "Invalid", + bsr.Protocol("INVALID"), + false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := bsr.ValidProtocol(tc.in) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/bsr/timestamp.go b/internal/bsr/timestamp.go new file mode 100644 index 0000000000..30bea681af --- /dev/null +++ b/internal/bsr/timestamp.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +import ( + "encoding/binary" + "time" +) + +const ( + secondSize = 8 + nanosecondSize = 4 + + timestampSize = secondSize + nanosecondSize +) + +// Timestamp is a time.Time that can be marshaled/unmarshaled to/from a bsr data file. +// A Timestamp in a bsr data file is represented as: +// +// uint64 seconds 8 bytes +// uint32 nanoseconds 4 bytes +// +// Where seconds is the number of seconds since unix epoch (Jan 1, 1970 00:00:00) +// and nanoseconds are the number of nanoseconds since the last second. +// This means the BSR cannot have times earlier than unix epoch. +type Timestamp time.Time + +// NewTimestamp creates a Timestamp. +func NewTimestamp(t time.Time) *Timestamp { + tt := Timestamp(t) + return &tt +} + +func (t *Timestamp) marshal() []byte { + tt := time.Time(*t) + seconds := uint64(tt.Unix()) + nanoseconds := uint32(tt.Nanosecond()) + + d := make([]byte, 0, timestampSize) + d = binary.BigEndian.AppendUint64(d, seconds) + d = binary.BigEndian.AppendUint32(d, nanoseconds) + return d +} + +// AsTime returns a time.Time for a Timestamp. +func (t *Timestamp) AsTime() time.Time { + return time.Time(*t) +}