From f6cde8d9122cfb8a413b8091ac5e06d6f16fb535 Mon Sep 17 00:00:00 2001
From: Will McCutchen <will@mccutch.org>
Date: Fri, 16 Jun 2017 16:47:57 -0700
Subject: [PATCH] Fix and test synthetic byte stream

---
 httpbin/handlers.go     |  7 ++--
 httpbin/helpers.go      | 46 +++++++++++++++------
 httpbin/helpers_test.go | 89 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 125 insertions(+), 17 deletions(-)

diff --git a/httpbin/handlers.go b/httpbin/handlers.go
index 701211c..159e403 100644
--- a/httpbin/handlers.go
+++ b/httpbin/handlers.go
@@ -559,10 +559,9 @@ func (h *HTTPBin) Range(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	content := &syntheticReadSeeker{
-		numBytes:    numBytes,
-		byteFactory: func(offset int64) byte { return byte(97 + (offset % 26)) },
-	}
+	content := newSyntheticByteStream(numBytes, func(offset int64) byte {
+		return byte(97 + (offset % 26))
+	})
 	var modtime time.Time
 	http.ServeContent(w, r, "", modtime, content)
 }
diff --git a/httpbin/helpers.go b/httpbin/helpers.go
index e1d94fd..2fa7a92 100644
--- a/httpbin/helpers.go
+++ b/httpbin/helpers.go
@@ -11,6 +11,7 @@ import (
 	"net/url"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 )
 
@@ -140,41 +141,60 @@ func parseBoundedDuration(input string, min, max time.Duration) (time.Duration,
 	return d, err
 }
 
-// syntheticReadSeeker implements the ReadSeeker interface to allow reading
+// syntheticByteStream implements the ReadSeeker interface to allow reading
 // arbitrary subsets of bytes up to a maximum size given a function for
 // generating the byte at a given offset.
-type syntheticReadSeeker struct {
-	numBytes    int64
-	offset      int64
-	byteFactory func(int64) byte
+type syntheticByteStream struct {
+	mu sync.Mutex
+
+	size    int64
+	offset  int64
+	factory func(int64) byte
+}
+
+// newSyntheticByteStream returns a new stream of bytes of a specific size,
+// given a factory function for generating the byte at a given offset.
+func newSyntheticByteStream(size int64, factory func(int64) byte) io.ReadSeeker {
+	return &syntheticByteStream{
+		size:    size,
+		factory: factory,
+	}
 }
 
-// Read implements the Reader interface for syntheticReadSeeker
-func (s *syntheticReadSeeker) Read(p []byte) (int, error) {
+// Read implements the Reader interface for syntheticByteStream
+func (s *syntheticByteStream) Read(p []byte) (int, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
 	start := s.offset
 	end := start + int64(len(p))
 	var err error
-	if end > s.numBytes {
+	if end >= s.size {
 		err = io.EOF
-		end = s.numBytes - start
+		end = s.size
 	}
 
 	for idx := start; idx < end; idx++ {
-		p[idx-start] = s.byteFactory(idx)
+		p[idx-start] = s.factory(idx)
 	}
 
+	s.offset = end
+
 	return int(end - start), err
 }
 
-// Seek implements the Seeker interface for syntheticReadSeeker
-func (s *syntheticReadSeeker) Seek(offset int64, whence int) (int64, error) {
+// Seek implements the Seeker interface for syntheticByteStream
+func (s *syntheticByteStream) Seek(offset int64, whence int) (int64, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
 	switch whence {
 	case io.SeekStart:
 		s.offset = offset
 	case io.SeekCurrent:
 		s.offset += offset
 	case io.SeekEnd:
-		s.offset = s.numBytes - offset
+		s.offset = s.size - offset
 	default:
 		return 0, errors.New("Seek: invalid whence")
 	}
diff --git a/httpbin/helpers_test.go b/httpbin/helpers_test.go
index 1422279..283d9b7 100644
--- a/httpbin/helpers_test.go
+++ b/httpbin/helpers_test.go
@@ -2,10 +2,36 @@ package httpbin
 
 import (
 	"fmt"
+	"io"
+	"reflect"
 	"testing"
 	"time"
 )
 
+func assertNil(t *testing.T, v interface{}) {
+	if v != nil {
+		t.Errorf("expected nil, got %#v", v)
+	}
+}
+
+func assertIntEqual(t *testing.T, a, b int) {
+	if a != b {
+		t.Errorf("expected %v == %v", a, b)
+	}
+}
+
+func assertBytesEqual(t *testing.T, a, b []byte) {
+	if !reflect.DeepEqual(a, b) {
+		t.Errorf("expected %v == %v", a, b)
+	}
+}
+
+func assertError(t *testing.T, got, expected error) {
+	if got != expected {
+		t.Errorf("expected error %v, got %v", expected, got)
+	}
+}
+
 func TestParseDuration(t *testing.T) {
 	var okTests = []struct {
 		input    string
@@ -53,3 +79,66 @@ func TestParseDuration(t *testing.T) {
 		})
 	}
 }
+
+func TestSyntheticByteStream(t *testing.T) {
+	factory := func(offset int64) byte {
+		return byte(offset)
+	}
+
+	t.Run("read", func(t *testing.T) {
+		s := newSyntheticByteStream(10, factory)
+
+		// read first half
+		p := make([]byte, 5)
+		count, err := s.Read(p)
+		assertNil(t, err)
+		assertIntEqual(t, count, 5)
+		assertBytesEqual(t, p, []byte{0, 1, 2, 3, 4})
+
+		// read second half
+		p = make([]byte, 5)
+		count, err = s.Read(p)
+		assertError(t, err, io.EOF)
+		assertIntEqual(t, count, 5)
+		assertBytesEqual(t, p, []byte{5, 6, 7, 8, 9})
+
+		// can't read any more
+		p = make([]byte, 5)
+		count, err = s.Read(p)
+		assertError(t, err, io.EOF)
+		assertIntEqual(t, count, 0)
+		assertBytesEqual(t, p, []byte{0, 0, 0, 0, 0})
+	})
+
+	t.Run("read into too-large buffer", func(t *testing.T) {
+		s := newSyntheticByteStream(5, factory)
+		p := make([]byte, 10)
+		count, err := s.Read(p)
+		assertError(t, err, io.EOF)
+		assertIntEqual(t, count, 5)
+		assertBytesEqual(t, p, []byte{0, 1, 2, 3, 4, 0, 0, 0, 0, 0})
+	})
+
+	t.Run("seek", func(t *testing.T) {
+		s := newSyntheticByteStream(100, factory)
+
+		p := make([]byte, 5)
+		s.Seek(10, io.SeekStart)
+		count, err := s.Read(p)
+		assertNil(t, err)
+		assertIntEqual(t, count, 5)
+		assertBytesEqual(t, p, []byte{10, 11, 12, 13, 14})
+
+		s.Seek(10, io.SeekCurrent)
+		count, err = s.Read(p)
+		assertNil(t, err)
+		assertIntEqual(t, count, 5)
+		assertBytesEqual(t, p, []byte{25, 26, 27, 28, 29})
+
+		s.Seek(10, io.SeekEnd)
+		count, err = s.Read(p)
+		assertNil(t, err)
+		assertIntEqual(t, count, 5)
+		assertBytesEqual(t, p, []byte{90, 91, 92, 93, 94})
+	})
+}
-- 
GitLab