diff --git a/reader/reusable_read_closer.go b/reader/reusable_read_closer.go index b33579d..9ec745d 100644 --- a/reader/reusable_read_closer.go +++ b/reader/reusable_read_closer.go @@ -25,12 +25,12 @@ func NewReusableReadCloser(raw interface{}) (*ReusableReadCloser, error) { switch body := raw.(type) { case []byte: - // if a byte array , create buffer from bytes and use it - readBuf = *bytes.NewBuffer(body) + // copy: reset writes the buffer back, which must not reach the + // caller's slice or another reader built from it + readBuf = *bytes.NewBuffer(bytes.Clone(body)) case *[]byte: - // if *[]byte, create buffer from bytes and use it - readBuf = *bytes.NewBuffer(*body) + readBuf = *bytes.NewBuffer(bytes.Clone(*body)) case string: // if a string , create buffer from string and use it diff --git a/reader/reusable_read_closer_test.go b/reader/reusable_read_closer_test.go index d5c75e5..a55ae35 100644 --- a/reader/reusable_read_closer_test.go +++ b/reader/reusable_read_closer_test.go @@ -35,7 +35,7 @@ func TestReusableReader(t *testing.T) { } }) - // todo: readers shouldn't be used concurrently, so here we just try to catch pontential read concurring with resets panics + // todo: readers shouldn't be used concurrently, so here we just try to catch potential read concurring with resets panics t.Run("concurrent-reset-with-read", func(t *testing.T) { reusableReader, err := NewReusableReadCloser(v) require.Nil(t, err) @@ -58,3 +58,41 @@ func TestReusableReader(t *testing.T) { }) } } + +// Readers must own their bytes. reset writes the buffer back at EOF, so a +// caller that reuses the slice, and a second reader built from it, must still +// see the original body. +func TestReusableReaderCopiesByteSlices(t *testing.T) { + for name, build := range map[string]func([]byte) (*ReusableReadCloser, error){ + "[]byte": func(b []byte) (*ReusableReadCloser, error) { return NewReusableReadCloser(b) }, + "*[]byte": func(b []byte) (*ReusableReadCloser, error) { return NewReusableReadCloser(&b) }, + } { + t.Run(name, func(t *testing.T) { + body := []byte("original") + reader, err := build(body) + require.NoError(t, err) + copy(body, "modified") + + got, err := io.ReadAll(reader) + require.NoError(t, err) + require.Equal(t, "original", string(got)) + require.Equal(t, "modified", string(body)) + }) + + t.Run(name+" two readers", func(t *testing.T) { + body := []byte("original") + first, err := build(body) + require.NoError(t, err) + second, err := build(body) + require.NoError(t, err) + copy(body, "modified") + + for _, reader := range []*ReusableReadCloser{first, second, first} { + got, err := io.ReadAll(reader) + require.NoError(t, err) + require.Equal(t, "original", string(got)) + } + require.Equal(t, "modified", string(body)) + }) + } +}