From ffd00f8fc1774faa2e9beeae2c19a60e59c771e4 Mon Sep 17 00:00:00 2001 From: Brian Maher Date: Fri, 26 Jul 2019 07:03:55 -0700 Subject: [PATCH 1/4] Add `ScanRangeHeader` API. --- httputil/api/range.go | 68 ++++++++++++++++++++++++++++++++++++++ httputil/api/range_test.go | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 httputil/api/range.go create mode 100644 httputil/api/range_test.go diff --git a/httputil/api/range.go b/httputil/api/range.go new file mode 100644 index 0000000..204961f --- /dev/null +++ b/httputil/api/range.go @@ -0,0 +1,68 @@ +package api + +import ( + "fmt" + "strings" + "regexp" + "strconv" +) + +type RangeSpec struct { + First *int64 + Last *int64 + // If non-null First/Last are null. + SuffixLength *int64 +} + +type RangeHeader struct { + Unit string // always "bytes" for now + Specs []RangeSpec +} + +var rangeSpecRegex = regexp.MustCompile(`^\s*(\d*)\s*-\s*(\d*)\s*$`) + +func ScanRangeHeader(header string) (*RangeHeader, error) { + if len(header) == 0 { + return nil, nil + } + eq := strings.Index(header, "=") + if eq < 1 { + return nil, fmt.Errorf("Invalid Range header") + } + unit := strings.TrimSpace(header[0:eq]) + if "bytes" != unit { + return nil, fmt.Errorf("Unsupported Range header unit=%s", unit) + } + specStrs := strings.Split(header[eq+1:], ",") + var specs []RangeSpec + for _, specStr := range specStrs { + matches := rangeSpecRegex.FindStringSubmatch(specStr) + if len(matches) <= 0 { + return nil, fmt.Errorf("Invalid Range header, expected %s to be digits followed by '-' followed by digits", specStr) + } + var first, last, suffixLength *int64 + if len(matches[1]) > 0 { + v, _ := strconv.ParseInt(matches[1], 10, 64) + first = &v + if len(matches[2]) > 0 { + v2, _ := strconv.ParseInt(matches[2], 10, 64) + last = &v2 + } + } else if len(matches[2]) > 0 { + v, _ := strconv.ParseInt(matches[2], 10, 64) + suffixLength = &v + } else { + return nil, fmt.Errorf("Invalid Range header, expected more than just a '-'") + } + specs = append(specs, RangeSpec { + First: first, + Last: last, + SuffixLength: suffixLength, + }) + + } + return &RangeHeader { + Unit: unit, + Specs: specs, + }, nil +} diff --git a/httputil/api/range_test.go b/httputil/api/range_test.go new file mode 100644 index 0000000..0591088 --- /dev/null +++ b/httputil/api/range_test.go @@ -0,0 +1,61 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func newInt64(x int64) *int64 { + return &x +} + +func TestScanRangeHeader(t *testing.T) { + tests := []struct { + Header string + Error error + Expected *RangeHeader + }{ + { + Header: ` bytes = 1 - 3 , 10 - `, + Expected: &RangeHeader { + Unit: "bytes", + Specs: []RangeSpec { + { + First: newInt64(1), + Last: newInt64(3), + }, + { + First: newInt64(10), + }, + }, + }, + }, + { + Header: ` bytes = - 1000`, + Expected: &RangeHeader { + Unit: "bytes", + Specs: []RangeSpec { + { + SuffixLength: newInt64(1000), + }, + }, + }, + }, + { + Header: ` lines =1-2`, + Error: fmt.Errorf(`Unsupported Range header unit=lines`), + }, + { + Header: `bytes=-`, + Error: fmt.Errorf(`Invalid Range header, expected more than just a '-'`), + }, + } + for _, test := range tests { + spec, err := ScanRangeHeader(test.Header) + + assert.Equal(t, test.Error, err, "for header %q", test.Header) + assert.Equal(t, test.Expected, spec, "for header %q", test.Header) + } +} From fdce6380156dbd4b509edea183f226e54583908e Mon Sep 17 00:00:00 2001 From: Brian Maher Date: Fri, 26 Jul 2019 07:27:35 -0700 Subject: [PATCH 2/4] Return an error if a byte range contains a last-byte-pos < first-byte-pos. --- httputil/api/range.go | 4 ++++ httputil/api/range_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/httputil/api/range.go b/httputil/api/range.go index 204961f..22bce70 100644 --- a/httputil/api/range.go +++ b/httputil/api/range.go @@ -46,6 +46,10 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { first = &v if len(matches[2]) > 0 { v2, _ := strconv.ParseInt(matches[2], 10, 64) + if v2 < *first { + return nil, fmt.Errorf( + `Unsatisfiable byte range %s`, specStr) + } last = &v2 } } else if len(matches[2]) > 0 { diff --git a/httputil/api/range_test.go b/httputil/api/range_test.go index 0591088..64fd409 100644 --- a/httputil/api/range_test.go +++ b/httputil/api/range_test.go @@ -43,6 +43,10 @@ func TestScanRangeHeader(t *testing.T) { }, }, }, + { + Header: `bytes=10-1`, + Error: fmt.Errorf(`Unsatisfiable byte range 10-1`), + }, { Header: ` lines =1-2`, Error: fmt.Errorf(`Unsupported Range header unit=lines`), From 5e4439b7d93ac3a22238ca87c24a6889e7fac6bf Mon Sep 17 00:00:00 2001 From: Brian Maher Date: Fri, 26 Jul 2019 09:37:37 -0700 Subject: [PATCH 3/4] Return a structured error from ScanRangeHeader(). --- httputil/api/range.go | 55 +++++++++++++++++++++++++++++++------- httputil/api/range_test.go | 16 ++++++++--- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/httputil/api/range.go b/httputil/api/range.go index 22bce70..3fe9f31 100644 --- a/httputil/api/range.go +++ b/httputil/api/range.go @@ -1,12 +1,31 @@ package api import ( + "errors" "fmt" "strings" "regexp" "strconv" ) +type RangeErrorCode string +const ( + InvalidRangeHeader RangeErrorCode = "InvalidRangeHeader" + UnsupportedRangeUnit RangeErrorCode = "UnsupportedRangeUnit" + UnsatisfiableRange RangeErrorCode = "UnsatisfiableRange" +) + +type RangeError struct { + Code RangeErrorCode + Message string +} + +func (e *RangeError) Error() string { + return e.Message +} + +var ErrRangeHeaderInvalid = errors.New("Invalid Range header") + type RangeSpec struct { First *int64 Last *int64 @@ -27,18 +46,28 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { } eq := strings.Index(header, "=") if eq < 1 { - return nil, fmt.Errorf("Invalid Range header") + return nil, &RangeError { + Code: InvalidRangeHeader, + Message: "Expected Range header to begin with `bytes=`", + } } unit := strings.TrimSpace(header[0:eq]) if "bytes" != unit { - return nil, fmt.Errorf("Unsupported Range header unit=%s", unit) + return nil, &RangeError { + Code: UnsupportedRangeUnit, + Message: fmt.Sprintf("Unsupported Range header unit=%s", unit), + } } specStrs := strings.Split(header[eq+1:], ",") - var specs []RangeSpec - for _, specStr := range specStrs { + specs := make([]RangeSpec, len(specStrs)) + for i, specStr := range specStrs { matches := rangeSpecRegex.FindStringSubmatch(specStr) if len(matches) <= 0 { - return nil, fmt.Errorf("Invalid Range header, expected %s to be digits followed by '-' followed by digits", specStr) + return nil, &RangeError { + Code: InvalidRangeHeader, + Message: fmt.Sprintf( + "Invalid Range header, expected %s to be digits followed by '-' followed by digits", specStr), + } } var first, last, suffixLength *int64 if len(matches[1]) > 0 { @@ -47,8 +76,11 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { if len(matches[2]) > 0 { v2, _ := strconv.ParseInt(matches[2], 10, 64) if v2 < *first { - return nil, fmt.Errorf( - `Unsatisfiable byte range %s`, specStr) + return nil, &RangeError { + Code: UnsatisfiableRange, + Message: fmt.Sprintf( + `Unsatisfiable byte range %s`, specStr), + } } last = &v2 } @@ -56,13 +88,16 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { v, _ := strconv.ParseInt(matches[2], 10, 64) suffixLength = &v } else { - return nil, fmt.Errorf("Invalid Range header, expected more than just a '-'") + return nil, &RangeError { + Code: InvalidRangeHeader, + Message: "Invalid Range header, expected more than just a '-'", + } } - specs = append(specs, RangeSpec { + specs[i] = RangeSpec { First: first, Last: last, SuffixLength: suffixLength, - }) + } } return &RangeHeader { diff --git a/httputil/api/range_test.go b/httputil/api/range_test.go index 64fd409..9aa328e 100644 --- a/httputil/api/range_test.go +++ b/httputil/api/range_test.go @@ -1,7 +1,6 @@ package api import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -45,15 +44,24 @@ func TestScanRangeHeader(t *testing.T) { }, { Header: `bytes=10-1`, - Error: fmt.Errorf(`Unsatisfiable byte range 10-1`), + Error: &RangeError { + Code: UnsatisfiableRange, + Message: `Unsatisfiable byte range 10-1`, + }, }, { Header: ` lines =1-2`, - Error: fmt.Errorf(`Unsupported Range header unit=lines`), + Error: &RangeError { + Code: UnsupportedRangeUnit, + Message: `Unsupported Range header unit=lines`, + }, }, { Header: `bytes=-`, - Error: fmt.Errorf(`Invalid Range header, expected more than just a '-'`), + Error: &RangeError { + Code: InvalidRangeHeader, + Message: `Invalid Range header, expected more than just a '-'`, + }, }, } for _, test := range tests { From 254a9dc58e49a3bdd21bce1932bcffb137554b50 Mon Sep 17 00:00:00 2001 From: Brian Maher Date: Fri, 26 Jul 2019 10:27:34 -0700 Subject: [PATCH 4/4] Formatting change. --- httputil/api/range.go | 41 +++++++++++++++++++------------------- httputil/api/range_test.go | 14 ++++++------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/httputil/api/range.go b/httputil/api/range.go index 3fe9f31..36ca126 100644 --- a/httputil/api/range.go +++ b/httputil/api/range.go @@ -3,20 +3,21 @@ package api import ( "errors" "fmt" - "strings" "regexp" "strconv" + "strings" ) type RangeErrorCode string + const ( - InvalidRangeHeader RangeErrorCode = "InvalidRangeHeader" + InvalidRangeHeader RangeErrorCode = "InvalidRangeHeader" UnsupportedRangeUnit RangeErrorCode = "UnsupportedRangeUnit" - UnsatisfiableRange RangeErrorCode = "UnsatisfiableRange" + UnsatisfiableRange RangeErrorCode = "UnsatisfiableRange" ) type RangeError struct { - Code RangeErrorCode + Code RangeErrorCode Message string } @@ -27,8 +28,8 @@ func (e *RangeError) Error() string { var ErrRangeHeaderInvalid = errors.New("Invalid Range header") type RangeSpec struct { - First *int64 - Last *int64 + First *int64 + Last *int64 // If non-null First/Last are null. SuffixLength *int64 } @@ -46,14 +47,14 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { } eq := strings.Index(header, "=") if eq < 1 { - return nil, &RangeError { - Code: InvalidRangeHeader, + return nil, &RangeError{ + Code: InvalidRangeHeader, Message: "Expected Range header to begin with `bytes=`", } } unit := strings.TrimSpace(header[0:eq]) if "bytes" != unit { - return nil, &RangeError { + return nil, &RangeError{ Code: UnsupportedRangeUnit, Message: fmt.Sprintf("Unsupported Range header unit=%s", unit), } @@ -63,8 +64,8 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { for i, specStr := range specStrs { matches := rangeSpecRegex.FindStringSubmatch(specStr) if len(matches) <= 0 { - return nil, &RangeError { - Code: InvalidRangeHeader, + return nil, &RangeError{ + Code: InvalidRangeHeader, Message: fmt.Sprintf( "Invalid Range header, expected %s to be digits followed by '-' followed by digits", specStr), } @@ -76,8 +77,8 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { if len(matches[2]) > 0 { v2, _ := strconv.ParseInt(matches[2], 10, 64) if v2 < *first { - return nil, &RangeError { - Code: UnsatisfiableRange, + return nil, &RangeError{ + Code: UnsatisfiableRange, Message: fmt.Sprintf( `Unsatisfiable byte range %s`, specStr), } @@ -88,20 +89,20 @@ func ScanRangeHeader(header string) (*RangeHeader, error) { v, _ := strconv.ParseInt(matches[2], 10, 64) suffixLength = &v } else { - return nil, &RangeError { + return nil, &RangeError{ Code: InvalidRangeHeader, Message: "Invalid Range header, expected more than just a '-'", } } - specs[i] = RangeSpec { - First: first, - Last: last, + specs[i] = RangeSpec{ + First: first, + Last: last, SuffixLength: suffixLength, } - + } - return &RangeHeader { - Unit: unit, + return &RangeHeader{ + Unit: unit, Specs: specs, }, nil } diff --git a/httputil/api/range_test.go b/httputil/api/range_test.go index 9aa328e..bc0bca5 100644 --- a/httputil/api/range_test.go +++ b/httputil/api/range_test.go @@ -18,9 +18,9 @@ func TestScanRangeHeader(t *testing.T) { }{ { Header: ` bytes = 1 - 3 , 10 - `, - Expected: &RangeHeader { + Expected: &RangeHeader{ Unit: "bytes", - Specs: []RangeSpec { + Specs: []RangeSpec{ { First: newInt64(1), Last: newInt64(3), @@ -33,9 +33,9 @@ func TestScanRangeHeader(t *testing.T) { }, { Header: ` bytes = - 1000`, - Expected: &RangeHeader { + Expected: &RangeHeader{ Unit: "bytes", - Specs: []RangeSpec { + Specs: []RangeSpec{ { SuffixLength: newInt64(1000), }, @@ -44,21 +44,21 @@ func TestScanRangeHeader(t *testing.T) { }, { Header: `bytes=10-1`, - Error: &RangeError { + Error: &RangeError{ Code: UnsatisfiableRange, Message: `Unsatisfiable byte range 10-1`, }, }, { Header: ` lines =1-2`, - Error: &RangeError { + Error: &RangeError{ Code: UnsupportedRangeUnit, Message: `Unsupported Range header unit=lines`, }, }, { Header: `bytes=-`, - Error: &RangeError { + Error: &RangeError{ Code: InvalidRangeHeader, Message: `Invalid Range header, expected more than just a '-'`, },