From 627ea6a7880fae23b0a370482f007e81fe20ff71 Mon Sep 17 00:00:00 2001 From: Dylan Bourque Date: Tue, 2 Jan 2024 11:41:18 -0600 Subject: [PATCH] feat: add `Seek()` method to `Decoder` add new `Seek()` method (satisfying `io.Seeker`) to `Decoder` with associated tests also add `Mode()` to expose the read mode (fast vs safe) to consumers Issue: #136 --- decoder.go | 33 +++++++++++ decoder_test.go | 145 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/decoder.go b/decoder.go index a2b5299..58df307 100644 --- a/decoder.go +++ b/decoder.go @@ -66,11 +66,44 @@ func NewDecoder(p []byte) *Decoder { } } +// Mode returns the current decoding mode, safe vs fastest. +func (d *Decoder) Mode() DecoderMode { + return d.mode +} + // SetMode configures the decoding behavior, safe vs fastest. func (d *Decoder) SetMode(m DecoderMode) { d.mode = m } +// Seek sets the position of the next read operation to [offset], interpreted according to [whence]: +// [io.SeekStart] means relative to the start of the data, [io.SeekCurrent] means relative to the +// current offset, and [io.SeekEnd] means relative to the end. +// +// This low-level operation is provided to support advanced/custom usages of the decoder and it is up +// to the caller to ensure that the resulting offset will point to a valid location in the data stream. +func (d *Decoder) Seek(offset int64, whence int) (int64, error) { + pos := int(offset) + switch whence { + case io.SeekStart: + // no adjustment needed + case io.SeekCurrent: + // shift relative to current read offset + pos += d.offset + case io.SeekEnd: + // shift relative to EOF + pos += len(d.p) + default: + return int64(d.offset), fmt.Errorf("invalid value (%d) for whence", whence) + } + // verify bounds then update the read position + if pos < 0 || pos > len(d.p) { + return int64(d.offset), fmt.Errorf("seek position (%d) out of bounds", pos) + } + d.offset = pos + return int64(d.offset), nil +} + // Reset moves the read offset back to the beginning of the encoded data func (d *Decoder) Reset() { d.offset = 0 diff --git a/decoder_test.go b/decoder_test.go index 84c89d3..b036268 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -11,6 +11,151 @@ import ( "github.com/CrowdStrike/csproto" ) +func TestDecoderSeek(t *testing.T) { + testData := []byte{0x08, 0x01, 0x10, 0x00, 0x1A, 0xE, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74} + dec := csproto.NewDecoder(testData) + + t.Run("seek start", func(t *testing.T) { + t.Run("negative seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + startPos := int64(dec.Offset()) + pos, err := dec.Seek(-1, io.SeekStart) + assert.Error(t, err, "cannot seek to before BOF") + assert.Equal(t, startPos, pos, "read position should not change") + }) + t.Run("zero seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + pos, err := dec.Seek(0, io.SeekStart) + assert.NoError(t, err) + assert.Equal(t, int64(0), pos, "new read position should be BOF") + }) + t.Run("invalid positive seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + startPos := int64(dec.Offset()) + pos, err := dec.Seek(int64(len(testData)+1), io.SeekStart) + assert.Error(t, err, "cannot seek to after EOF") + assert.Equal(t, startPos, pos, "read position should not change") + }) + t.Run("valid positive seek", func(t *testing.T) { + dec.Reset() + + pos, err := dec.Seek(2, io.SeekStart) + assert.NoError(t, err) + assert.Equal(t, int(pos), dec.Offset()) + assert.True(t, dec.More()) + }) + }) + t.Run("seek current", func(t *testing.T) { + t.Run("invalid negative seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + startPos := int64(dec.Offset()) + pos, err := dec.Seek(-1*(startPos+1), io.SeekCurrent) + assert.Error(t, err, "cannot seek to before BOF") + assert.Equal(t, startPos, pos, "read position should not change") + }) + t.Run("valid negative seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + startPos := int64(dec.Offset()) + pos, err := dec.Seek(-1*startPos, io.SeekCurrent) + assert.NoError(t, err) + assert.Equal(t, int64(0), pos) + }) + t.Run("zero seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + startPos := int64(dec.Offset()) + pos, err := dec.Seek(0, io.SeekCurrent) + assert.NoError(t, err) + assert.Equal(t, int64(startPos), pos) + }) + t.Run("invalid positive seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + startPos := int64(dec.Offset()) + pos, err := dec.Seek(int64(len(testData)), io.SeekCurrent) + assert.Error(t, err, "cannot seek to after EOF") + assert.Equal(t, startPos, pos, "read position should not change") + }) + t.Run("valid positive seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + pos, err := dec.Seek(2, io.SeekCurrent) + assert.NoError(t, err) + assert.Equal(t, int(pos), dec.Offset()) + assert.True(t, dec.More()) + }) + }) + t.Run("seek end", func(t *testing.T) { + t.Run("positive seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + startPos := dec.Offset() + + pos, err := dec.Seek(1, io.SeekEnd) + assert.Error(t, err, "cannot seek to after EOF") + assert.Equal(t, int64(startPos), pos, "read position should not change") + }) + t.Run("zero seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + pos, err := dec.Seek(0, io.SeekEnd) + assert.NoError(t, err) + assert.Equal(t, int64(len(testData)), pos, "read position should be at EOF") + assert.False(t, dec.More()) + }) + t.Run("invalid negative seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + startPos := dec.Offset() + + pos, err := dec.Seek(int64(-1*(len(testData)+1)), io.SeekEnd) + assert.Error(t, err, "cannot seek to before BOF") + assert.Equal(t, int64(startPos), pos, "read position should be at EOF") + }) + t.Run("valid negative seek", func(t *testing.T) { + dec.Reset() + _, _, _ = dec.DecodeTag() + _, _ = dec.DecodeBool() + + pos, err := dec.Seek(-16, io.SeekEnd) + assert.NoError(t, err) + assert.Equal(t, int64(4), pos, "read position should be at EOF") + }) + }) + t.Run("invalid whence", func(t *testing.T) { + dec.Reset() + startPos := dec.Offset() + pos, err := dec.Seek(0, 1138) + assert.Error(t, err, "cannot seek with invalid 'whence'") + assert.Equal(t, int64(startPos), pos, "read position should not change") + }) +} + func TestDecodeBool(t *testing.T) { cases := []struct { name string