From 9a73688e3aa8dd015155e12fcf8b98957e43be4c Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Sat, 27 Jan 2018 09:27:50 +0000 Subject: [PATCH] fs: Add ParseRangeOption to parse incoming Range: requests --- fs/options.go | 34 ++++++++++++++++++++++++++++++++++ fs/options_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 fs/options_test.go diff --git a/fs/options.go b/fs/options.go index 3d51001d9..66345466d 100644 --- a/fs/options.go +++ b/fs/options.go @@ -6,8 +6,10 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/ncw/rclone/fs/hash" + "github.com/pkg/errors" ) // OpenOption is an interface describing options for Open @@ -52,6 +54,38 @@ func (o *RangeOption) Header() (key string, value string) { return key, value } +// ParseRangeOption parses a RangeOption from a Range: header. +// It only appects single ranges. +func ParseRangeOption(s string) (po *RangeOption, err error) { + const preamble = "bytes=" + if !strings.HasPrefix(s, preamble) { + return nil, errors.New("Range: header invalid: doesn't start with " + preamble) + } + s = s[len(preamble):] + if strings.IndexRune(s, ',') >= 0 { + return nil, errors.New("Range: header invalid: contains multiple ranges which isn't supported") + } + dash := strings.IndexRune(s, '-') + if dash < 0 { + return nil, errors.New("Range: header invalid: contains no '-'") + } + start, end := strings.TrimSpace(s[:dash]), strings.TrimSpace(s[dash+1:]) + o := RangeOption{Start: -1, End: -1} + if start != "" { + o.Start, err = strconv.ParseInt(start, 10, 64) + if err != nil || o.Start < 0 { + return nil, errors.New("Range: header invalid: bad start") + } + } + if end != "" { + o.End, err = strconv.ParseInt(end, 10, 64) + if err != nil || o.End < 0 { + return nil, errors.New("Range: header invalid: bad end") + } + } + return &o, nil +} + // String formats the option into human readable form func (o *RangeOption) String() string { return fmt.Sprintf("RangeOption(%d,%d)", o.Start, o.End) diff --git a/fs/options_test.go b/fs/options_test.go new file mode 100644 index 000000000..70ad2883a --- /dev/null +++ b/fs/options_test.go @@ -0,0 +1,39 @@ +package fs + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseRangeOption(t *testing.T) { + for _, test := range []struct { + in string + want RangeOption + err string + }{ + {in: "", err: "doesn't start with bytes="}, + {in: "bytes=1-2,3-4", err: "contains multiple ranges"}, + {in: "bytes=100", err: "contains no '-'"}, + {in: "bytes=x-8", err: "bad start"}, + {in: "bytes=8-x", err: "bad end"}, + {in: "bytes=1-2", want: RangeOption{Start: 1, End: 2}}, + {in: "bytes=-123456789123456789", want: RangeOption{Start: -1, End: 123456789123456789}}, + {in: "bytes=123456789123456789-", want: RangeOption{Start: 123456789123456789, End: -1}}, + {in: "bytes= 1 - 2 ", want: RangeOption{Start: 1, End: 2}}, + {in: "bytes=-", want: RangeOption{Start: -1, End: -1}}, + {in: "bytes= - ", want: RangeOption{Start: -1, End: -1}}, + } { + got, err := ParseRangeOption(test.in) + what := fmt.Sprintf("parsing %q", test.in) + if test.err != "" { + require.Contains(t, err.Error(), test.err) + require.Nil(t, got, what) + } else { + require.NoError(t, err, what) + assert.Equal(t, test.want, *got, what) + } + } +}