Skip to content

Commit

Permalink
Merge pull request #2 from toaster/bugfix
Browse files Browse the repository at this point in the history
Bugfixes
  • Loading branch information
romantomjak committed Apr 7, 2020
2 parents a730696 + c499d1f commit a3d084d
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 54 deletions.
96 changes: 67 additions & 29 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,12 @@ func Open(url string) (*Stream, error) {
log.Print("[DEBUG] HTTP header ", k, ": ", v[0])
}

bitrate, err := strconv.Atoi(resp.Header.Get("icy-br"))
if err != nil {
return nil, fmt.Errorf("cannot parse bitrate: %v", err)
var bitrate int
if rawBitrate := resp.Header.Get("icy-br"); rawBitrate != "" {
bitrate, err = strconv.Atoi(rawBitrate)
if err != nil {
return nil, fmt.Errorf("cannot parse bitrate: %v", err)
}
}

metaint, err := strconv.Atoi(resp.Header.Get("icy-metaint"))
Expand All @@ -96,39 +99,74 @@ func Open(url string) (*Stream, error) {
}

// Read implements the standard Read interface
func (s *Stream) Read(p []byte) (n int, err error) {
n, err = s.rc.Read(p)

if s.pos+n <= s.metaint {
s.pos = s.pos + n
return n, err
}

// extract stream metadata
metadataStart := s.metaint - s.pos
metadataLength := int(p[metadataStart : metadataStart+1][0]) * 16
if metadataLength > 0 {
m := NewMetadata(p[metadataStart+1 : metadataStart+1+metadataLength])
if !m.Equals(s.metadata) {
s.metadata = m
if s.MetadataCallbackFunc != nil {
s.MetadataCallbackFunc(s.metadata)
}
func (s *Stream) Read(buf []byte) (dataLen int, err error) {
dataLen, err = s.rc.Read(buf)

checkedDataLen := 0
uncheckedDataLen := dataLen
for s.pos+uncheckedDataLen > s.metaint {
offset := s.metaint - s.pos
skip, e := s.extractMetadata(buf[checkedDataLen+offset:])
if e != nil {
err = e
}
s.pos = 0
if offset+skip > uncheckedDataLen {
dataLen = checkedDataLen + offset
uncheckedDataLen = 0
} else {
checkedDataLen += offset
dataLen -= skip
uncheckedDataLen = dataLen - checkedDataLen
copy(buf[checkedDataLen:], buf[checkedDataLen+skip:])
}
}
s.pos = s.pos + uncheckedDataLen

// roll over position + metadata block
s.pos = ((s.pos + n) - s.metaint) - metadataLength - 1

// shift buffer data to account for metadata block
copy(p[metadataStart:], p[metadataStart+1+metadataLength:])
n = n - 1 - metadataLength

return n, err
return
}

// Close closes the stream
func (s *Stream) Close() error {
log.Print("[INFO] Closing ", s.URL)
return s.rc.Close()
}

func (s *Stream) extractMetadata(p []byte) (int, error) {
var metabuf []byte
var err error
length := int(p[0]) * 16
end := length + 1
complete := false
if length > 0 {
if len(p) < end {
// The provided buffer was not large enough for the metadata block to fit in.
// Read whole metadata into our own buffer.
metabuf = make([]byte, length)
copy(metabuf, p[1:])
n := len(p) - 1
for n < length && err == nil {
var nn int
nn, err = s.rc.Read(metabuf[n:])
n += nn
}
if n == length {
complete = true
} else if err == nil || err == io.EOF {
err = io.ErrUnexpectedEOF
}
} else {
metabuf = p[1:end]
complete = true
}
}
if complete {
if m := NewMetadata(metabuf); !m.Equals(s.metadata) {
s.metadata = m
if s.MetadataCallbackFunc != nil {
s.MetadataCallbackFunc(s.metadata)
}
}
}
return length + 1, err
}
204 changes: 179 additions & 25 deletions stream_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package shoutcast

import (
"fmt"
"io"
"math"
"net/http"
Expand All @@ -17,11 +16,38 @@ func assertStrings(t *testing.T, got, want string) {
}
}

func assertEqual(t *testing.T, got, want interface{}) {
func assertEqual(t *testing.T, got, want interface{}) bool {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
return false
}
return true
}

func assertNoError(t *testing.T, err error) bool {
t.Helper()
if err != nil {
t.Errorf("Received unexpected error:\n%+v", err)
return false
}
return true
}

func requireNoError(t *testing.T, err error) {
t.Helper()
if !assertNoError(t, err) {
t.FailNow()
}
}

func assertError(t *testing.T, err error) bool {
t.Helper()
if err == nil {
t.Error("An error is expected but got nil.")
return false
}
return true
}

func makeMetadata(s string) []byte {
Expand Down Expand Up @@ -73,32 +99,56 @@ func TestRequiredHTTPHeadersArePresent(t *testing.T) {
assertStrings(t, headers.Get("user-agent")[:6], "iTunes")
}

func TestMissingBitrate(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header()["icy-metaint"] = []string{"100"}
w.WriteHeader(200)
}))

_, err := Open(ts.URL)
assertNoError(t, err)
}

func TestUnexpectedEOF(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("icy-br", "192")
w.Header().Set("icy-metaint", "1")

metadata := makeMetadata("SongTitle='Prospa Prayer';")
stream := insertMetadata([]byte{1, 1}, metadata, 1)
fmt.Printf("%v\n", stream)
w.Write(stream)
// unexpected EOF in the middle of a metadata block
w.Write(stream[:len(stream)-10])
}))
defer ts.Close()

s, _ := Open(ts.URL)

b1 := make([]byte, 1)
s.Read(b1)
assertEqual(t, b1, []byte{1})
n, err := s.Read(b1)
if assertNoError(t, err) && assertEqual(t, 1, n) {
assertEqual(t, []byte{1}, b1)
}

// The metadata is read immediately and does not fit into the buffer.
// -> `0, nil` is returned.
// Filling the buffer after the reading of the metadata would be more complexity without advantage.
n, err = s.Read(b1)
assertNoError(t, err)
assertEqual(t, 0, n)

b2 := make([]byte, 1)
s.Read(b2)
assertEqual(t, b2, []byte{1})
n, err = s.Read(b2)
if assertNoError(t, err) && assertEqual(t, 1, n) {
assertEqual(t, []byte{1}, b2)
}

// ooops, nothing to read
// oops, nothing to read
b3 := make([]byte, 1)
_, err := s.Read(b3)
assertEqual(t, err, io.ErrUnexpectedEOF)
n, err = s.Read(b3)
assertEqual(t, 0, n)
if assertError(t, err) {
assertEqual(t, io.ErrUnexpectedEOF, err)
}
}

func TestMetaintEqualsClientBufferLength(t *testing.T) {
Expand All @@ -108,24 +158,48 @@ func TestMetaintEqualsClientBufferLength(t *testing.T) {

metadata := makeMetadata("SongTitle='Prospa Prayer';")
stream := insertMetadata([]byte{1, 1, 1, 1, 1, 1}, metadata, 2)
fmt.Printf("%v\n", stream)
w.Write(stream)
}))
defer ts.Close()

s, _ := Open(ts.URL)

b1 := make([]byte, 2)
s.Read(b1)
assertEqual(t, b1, []byte{1, 1})
n, err := s.Read(b1)
if assertNoError(t, err) && assertEqual(t, 2, n) {
assertEqual(t, []byte{1, 1}, b1)
}

// The metadata is read immediately and does not fit into the buffer.
// -> `0, nil` is returned.
// Filling the buffer after the reading of the metadata would be more complexity without advantage.
n, err = s.Read(b1)
assertNoError(t, err)
assertEqual(t, 0, n)

b2 := make([]byte, 2)
s.Read(b2)
assertEqual(t, b2, []byte{1, 1})
n, err = s.Read(b2)
if assertNoError(t, err) && assertEqual(t, 2, n) {
assertEqual(t, []byte{1, 1}, b2)
}

// no data except metadata read, again
n, err = s.Read(b2)
assertNoError(t, err)
assertEqual(t, 0, n)

b3 := make([]byte, 2)
s.Read(b3)
assertEqual(t, b3, []byte{1, 1})
n, err = s.Read(b3)
if assertNoError(t, err) && assertEqual(t, 2, n) {
assertEqual(t, []byte{1, 1}, b3)
}

// check for EOF
n, err = s.Read(b1)
assertEqual(t, 0, n)
if assertError(t, err) {
assertEqual(t, io.EOF, err)
}
}

func TestMetaintGreaterThanClientBufferLength(t *testing.T) {
Expand All @@ -135,24 +209,104 @@ func TestMetaintGreaterThanClientBufferLength(t *testing.T) {

metadata := makeMetadata("SongTitle='Prospa Prayer';")
stream := insertMetadata([]byte{1, 1, 1, 1, 1, 1}, metadata, 3)
fmt.Printf("%v\n", stream)
w.Write(stream)
}))
defer ts.Close()

s, _ := Open(ts.URL)

b1 := make([]byte, 2)
s.Read(b1)
assertEqual(t, b1, []byte{1, 1})
n, err := s.Read(b1)
if assertNoError(t, err) && assertEqual(t, 2, n) {
assertEqual(t, []byte{1, 1}, b1)
}

// only one byte read then follows metadata
b2 := make([]byte, 2)
s.Read(b2)
assertEqual(t, b2, []byte{1, 1})
n, err = s.Read(b2)
if assertNoError(t, err) && assertEqual(t, 1, n) {
// don't assert the second byte, only one read
assertEqual(t, []byte{1}, b2[:1])
}

b3 := make([]byte, 2)
s.Read(b3)
assertEqual(t, b3, []byte{1, 1})
n, err = s.Read(b3)
if assertNoError(t, err) && assertEqual(t, 2, n) {
assertEqual(t, []byte{1, 1}, b3)
}

// only one byte read then follows metadata and then EOF
b4 := make([]byte, 2)
n, err = s.Read(b4)
if assertEqual(t, 1, n) {
// don't assert the second byte, only one read
assertEqual(t, []byte{1}, b4[:1])
}
if assertError(t, err) {
assertEqual(t, io.EOF, err)
}
}

func TestClientBufferLargeEnoughForMetadata(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("icy-br", "192")
w.Header().Set("icy-metaint", "3")

metadata := makeMetadata("SongTitle='Prospa Prayer';")
stream := insertMetadata([]byte{3, 4, 5, 6, 7, 8, 9}, metadata, 3)
w.Write(stream)
}))
defer ts.Close()

s, err := Open(ts.URL)
requireNoError(t, err)

// metadata length is 33 (2*16+1) -> 38 - 33 = 5 bytes stream data to be read
b1 := make([]byte, 38)
n, err := s.Read(b1)
if assertNoError(t, err) && assertEqual(t, 5, n) {
assertEqual(t, []byte{3, 4, 5, 6, 7}, b1[:5])
}

b2 := make([]byte, 38)
n, err = s.Read(b2)
if assertEqual(t, 2, n) {
assertEqual(t, []byte{8, 9}, b2[:2])
}
if assertError(t, err) {
assertEqual(t, io.EOF, err)
}
}

func TestClientBufferLargeEnoughForTwoTimesMetadata(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("icy-br", "192")
w.Header().Set("icy-metaint", "3")

metadata := makeMetadata("SongTitle='Prospa Prayer';")
stream := insertMetadata([]byte{3, 4, 5, 6, 7, 8, 9, 10}, metadata, 3)
w.Write(stream)
}))
defer ts.Close()

s, err := Open(ts.URL)
requireNoError(t, err)

// metadata length is 33 (2*16+1) -> 73 - 2 * 33 = 7
b1 := make([]byte, 73)
n, err := s.Read(b1)
if assertNoError(t, err) && assertEqual(t, 7, n) {
assertEqual(t, []byte{3, 4, 5, 6, 7, 8, 9}, b1[:7])
}

b2 := make([]byte, 38)
n, err = s.Read(b2)
if assertEqual(t, 1, n) {
assertEqual(t, []byte{10}, b2[:1])
}
if assertError(t, err) {
assertEqual(t, io.EOF, err)
}
}

// test for EOF
Expand Down

0 comments on commit a3d084d

Please sign in to comment.