feat(bsr): Define initial interfaces and types

pull/3251/head
Timothy Messier 3 years ago
parent b73d4bd0d2
commit 658bb27eeb
No known key found for this signature in database
GPG Key ID: EFD2F184F7600572

@ -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
}

@ -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
}

@ -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)
})
}
}

@ -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
}

@ -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)
})
}
}

@ -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())
})
}
}

@ -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}
}

@ -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())
}

@ -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)
})
}
}

@ -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
}

@ -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)
})
}
}

@ -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

@ -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
}

@ -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)
})
}
}

@ -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)
}

@ -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"))
}

@ -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
}

@ -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)
})
}
}

@ -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)
}
Loading…
Cancel
Save