Repository: fsouza/fake-gcs-server (go)

Issue:
Add support for negative offset
It seems that negative offset is not considered, such as `range=-10-` which means `NewRangeReader(ctx, -10, -1)`.
https://github.com/fsouza/fake-gcs-server/blob/5a7dc5a5a672ab1b61ed7a9d4180614d60245975/fakestorage/object.go#L578-L599

It can be seen in `Example(LastNBytes)` in https://pkg.go.dev/cloud.google.com/go/storage#ObjectHandle.NewRangeReader

Pull request:
Handle offset-from-end ranges and make range handling more robust
This PR adds support for offset-from-end ranges. An offset-from-end
range header looks like "Range: bytes=-10". This returns content
starting 10 bytes from the end of the content to the end of the content.

The PR also updates other range responses to align with the real GCS
server. Specifically, the range handling code now detects when the start
of range is beyond the content bounds and returns a 416 response. Most
other invalid ranges are ignored, except for a few cases where bounds
are automatically adjusted.

All of the cases were verified against the GCS server using requests
like:
```
curl -D - https://storage.googleapis.com/marksandstrom-test/test.txt -H"Range: bytes=0-4"
```

Fixes #536.

Patch:
diff --git a/Dockerfile b/Dockerfile
index 0dd685c..bf9fafb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style
 # license that can be found in the LICENSE file.
 
-FROM golang:1.18.2 AS builder
+FROM golang:1.18.3 AS builder
 WORKDIR /code
 ADD go.mod go.sum ./
 RUN go mod download
diff --git a/examples/go/go.mod b/examples/go/go.mod
index 4e2ca91..5b0a618 100644
--- a/examples/go/go.mod
+++ b/examples/go/go.mod
@@ -4,5 +4,5 @@ go 1.14
 
 require (
 	cloud.google.com/go/storage v1.22.1
-	google.golang.org/api v0.81.0
+	google.golang.org/api v0.82.0
 )
diff --git a/examples/go/go.sum b/examples/go/go.sum
index ca7a87e..5ef618a 100644
--- a/examples/go/go.sum
+++ b/examples/go/go.sum
@@ -290,8 +290,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
-golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
+golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -310,8 +310,9 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -491,8 +492,8 @@ google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc
 google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
 google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
-google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8=
-google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
+google.golang.org/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw=
+google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -576,8 +577,8 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX
 google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
+google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/fakestorage/bucket.go b/fakestorage/bucket.go
index b6af52b..49aedb1 100644
--- a/fakestorage/bucket.go
+++ b/fakestorage/bucket.go
@@ -7,6 +7,7 @@ package fakestorage
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
 	"regexp"
 
@@ -70,6 +71,18 @@ func (s *Server) createBucketByPost(r *http.Request) jsonResponse {
 		return jsonResponse{errorMessage: err.Error(), status: http.StatusBadRequest}
 	}
 
+	_, err := s.backend.GetBucket(name)
+	if err == nil {
+		return jsonResponse{
+			errorMessage: fmt.Sprintf(
+				"A Cloud Storage bucket named '%s' already exists. "+
+					"Try another name. Bucket names must be globally unique "+
+					"across all Google Cloud projects, including those "+
+					"outside of your organization.", name),
+			status: http.StatusConflict,
+		}
+	}
+
 	// Create the named bucket
 	if err := s.backend.CreateBucket(name, versioning); err != nil {
 		return jsonResponse{errorMessage: err.Error()}
diff --git a/fakestorage/object.go b/fakestorage/object.go
index d4c518f..853fb2d 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -9,7 +9,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"math"
 	"net/http"
 	"sort"
 	"strconv"
@@ -648,50 +647,135 @@ func (s *Server) downloadObject(w http.ResponseWriter, r *http.Request) {
 	}
 
 	status := http.StatusOK
-	ranged, start, lastByte, content := s.handleRange(obj, r)
-	if ranged {
+	ranged, start, lastByte, content, satisfiable := s.handleRange(obj, r)
+
+	if ranged && satisfiable {
 		status = http.StatusPartialContent
 		w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, lastByte, len(obj.Content)))
 	}
-	if obj.ContentType != "" {
-		w.Header().Set(contentTypeHeader, obj.ContentType)
-	}
 	w.Header().Set("Accept-Ranges", "bytes")
 	w.Header().Set("Content-Length", strconv.Itoa(len(content)))
 	w.Header().Set("X-Goog-Generation", strconv.FormatInt(obj.Generation, 10))
 	w.Header().Set("X-Goog-Hash", fmt.Sprintf("crc32c=%s,md5=%s", obj.Crc32c, obj.Md5Hash))
 	w.Header().Set("Last-Modified", obj.Updated.Format(http.TimeFormat))
-	if obj.ContentEncoding != "" {
-		w.Header().Set("Content-Encoding", obj.ContentEncoding)
+
+	if ranged && !satisfiable {
+		status = http.StatusRequestedRangeNotSatisfiable
+		content = []byte(fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8'?>`+
+			`<Error><Code>InvalidRange</Code>`+
+			`<Message>The requested range cannot be satisfied.</Message>`+
+			`<Details>%s</Details></Error>`, r.Header.Get("Range")))
+		w.Header().Set(contentTypeHeader, "application/xml; charset=UTF-8")
+	} else {
+		if obj.ContentType != "" {
+			w.Header().Set(contentTypeHeader, obj.ContentType)
+		}
+		if obj.ContentEncoding != "" {
+			w.Header().Set("Content-Encoding", obj.ContentEncoding)
+		}
 	}
+
 	w.WriteHeader(status)
 	if r.Method == http.MethodGet {
 		w.Write(content)
 	}
 }
 
-func (s *Server) handleRange(obj Object, r *http.Request) (ranged bool, start int64, lastByte int64, content []byte) {
-	if reqRange := r.Header.Get("Range"); reqRange != "" {
-		parts := strings.SplitN(reqRange, "=", 2)
-		if len(parts) == 2 && parts[0] == "bytes" {
-			rangeParts := strings.SplitN(parts[1], "-", 2)
-			if len(rangeParts) == 2 {
-				start, _ = strconv.ParseInt(rangeParts[0], 10, 64)
-				var err error
-				var end int64
-				if end, err = strconv.ParseInt(rangeParts[1], 10, 64); err != nil {
-					end = int64(len(obj.Content))
-				} else if end != math.MaxInt64 {
-					end++
-				}
-				if end > int64(len(obj.Content)) {
-					end = int64(len(obj.Content))
-				}
-				return true, start, end - 1, obj.Content[start:end]
+func (s *Server) handleRange(obj Object, r *http.Request) (ranged bool, start int64, lastByte int64, content []byte, satisfiable bool) {
+	contentLength := int64(len(obj.Content))
+	start, end, err := parseRange(r.Header.Get("Range"), contentLength)
+	if err != nil {
+		// If the range isn't valid, GCS returns all content.
+		return false, 0, 0, obj.Content, false
+	}
+	// GCS is pretty flexible when it comes to invalid ranges. A 416 http
+	// response is only returned when the range start is beyond the length of
+	// the content. Otherwise, the range is ignored.
+	switch {
+	// Invalid start. Return 416 and NO content.
+	// Examples:
+	//   Length: 40, Range: bytes=50-60
+	//   Length: 40, Range: bytes=50-
+	case start >= contentLength:
+		// This IS a ranged request, but it ISN'T satisfiable.
+		return true, 0, 0, []byte{}, false
+	// Negative range, ignore range and return all content.
+	// Examples:
+	//   Length: 40, Range: bytes=30-20
+	case end < start:
+		return false, 0, 0, obj.Content, false
+	// Return range. Clamp start and end.
+	// Examples:
+	//   Length: 40, Range: bytes=-100
+	//   Length: 40, Range: bytes=0-100
+	default:
+		if start < 0 {
+			start = 0
+		}
+		if end >= contentLength {
+			end = contentLength - 1
+		}
+		return true, start, end, obj.Content[start : end+1], true
+	}
+}
+
+// parseRange parses the range header and returns the corresponding start and
+// end indices in the content. The end index is inclusive. This function
+// doesn't validate that the start and end indices fall within the content
+// bounds. The content length is only used to handle "suffix length" and
+// range-to-end ranges.
+func parseRange(rangeHeaderValue string, contentLength int64) (start int64, end int64, err error) {
+	// For information about the range header, see:
+	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
+	// https://httpwg.org/specs/rfc7233.html#header.range
+	// https://httpwg.org/specs/rfc7233.html#byte.ranges
+	// https://httpwg.org/specs/rfc7233.html#status.416
+	//
+	// <unit>=<range spec>
+	//
+	// The following ranges are parsed:
+	// "bytes=40-50" (range with given start and end)
+	// "bytes=40-"   (range to end of content)
+	// "bytes=-40"   (suffix length, offset from end of string)
+	//
+	// The unit MUST be "bytes".
+	parts := strings.SplitN(rangeHeaderValue, "=", 2)
+	if len(parts) != 2 {
+		return 0, 0, fmt.Errorf("expecting `=` in range header, got: %s", rangeHeaderValue)
+	}
+	if parts[0] != "bytes" {
+		return 0, 0, fmt.Errorf("invalid range unit, expecting `bytes`, got: %s", parts[0])
+	}
+	rangeSpec := parts[1]
+	if len(rangeSpec) == 0 {
+		return 0, 0, errors.New("empty range")
+	}
+	if rangeSpec[0] == '-' {
+		offsetFromEnd, err := strconv.ParseInt(rangeSpec, 10, 64)
+		if err != nil {
+			return 0, 0, fmt.Errorf("invalid suffix length, got: %s", rangeSpec)
+		}
+		start = contentLength + offsetFromEnd
+		end = contentLength - 1
+	} else {
+		rangeParts := strings.SplitN(rangeSpec, "-", 2)
+		if len(rangeParts) != 2 {
+			return 0, 0, fmt.Errorf("only one range supported, got: %s", rangeSpec)
+		}
+		start, err = strconv.ParseInt(rangeParts[0], 10, 64)
+		if err != nil {
+			return 0, 0, fmt.Errorf("invalid range start, got: %s", rangeParts[0])
+		}
+		if rangeParts[1] == "" {
+			end = contentLength - 1
+		} else {
+			end, err = strconv.ParseInt(rangeParts[1], 10, 64)
+			if err != nil {
+				return 0, 0, fmt.Errorf("invalid range end, got: %s", rangeParts[1])
 			}
 		}
 	}
-	return false, 0, 0, obj.Content
+	return start, end, nil
 }
 
 func (s *Server) patchObject(r *http.Request) jsonResponse {
diff --git a/go.mod b/go.mod
index 2285dd6..c05f2f1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,7 @@
 module github.com/fsouza/fake-gcs-server
 
 require (
-	cloud.google.com/go/pubsub v1.21.1
+	cloud.google.com/go/pubsub v1.22.1
 	cloud.google.com/go/storage v1.22.1
 	github.com/google/go-cmp v0.5.8
 	github.com/gorilla/handlers v1.5.1
@@ -9,12 +9,12 @@ require (
 	github.com/pkg/xattr v0.4.7
 	github.com/sirupsen/logrus v1.8.1
 	github.com/stretchr/testify v1.7.1
-	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
-	google.golang.org/api v0.81.0
+	golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
+	google.golang.org/api v0.82.0
 )
 
 require (
-	cloud.google.com/go v0.100.2 // indirect
+	cloud.google.com/go v0.102.0 // indirect
 	cloud.google.com/go/compute v1.6.1 // indirect
 	cloud.google.com/go/iam v0.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -26,13 +26,13 @@ require (
 	github.com/googleapis/go-type-adapters v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	go.opencensus.io v0.23.0 // indirect
-	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
+	golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
 	golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
 	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
+	google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
 	google.golang.org/grpc v1.46.2 // indirect
 	google.golang.org/protobuf v1.28.0 // indirect
 	gopkg.in/yaml.v3 v3.0.0 // indirect
diff --git a/go.sum b/go.sum
index 97ce282..001dbff 100644
--- a/go.sum
+++ b/go.sum
@@ -27,8 +27,9 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW
 cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
 cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
 cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
-cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
 cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
+cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8=
+cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -52,8 +53,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
 cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/pubsub v1.21.1 h1:ghu6wlm6WouITmmuwkxGG+6vNRXDaPdAjqLcRdsw3EQ=
-cloud.google.com/go/pubsub v1.21.1/go.mod h1:u3XGeMBOBCIQLcxNzy14Svz88ZFS8vI250uDgIAQDSQ=
+cloud.google.com/go/pubsub v1.22.1 h1:YdmltGCbqrXdwWE4KQ8EjoFizmtmCgZ4J0nP7JX9K+U=
+cloud.google.com/go/pubsub v1.22.1/go.mod h1:LBHGrtgM7+SGKCDKQu2pKIRtGwbZyJvRDkMk0594xdU=
 cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
 cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
@@ -315,8 +316,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
 golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
+golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -335,8 +337,9 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -407,6 +410,7 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -519,10 +523,11 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S
 google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
 google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
 google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
-google.golang.org/api v0.76.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
-google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8=
+google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
 google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
+google.golang.org/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw=
+google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -603,12 +608,13 @@ google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX
 google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
 google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
+google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=


Test patch:
diff --git a/fakestorage/bucket_test.go b/fakestorage/bucket_test.go
index 85fb68c..8ea8903 100644
--- a/fakestorage/bucket_test.go
+++ b/fakestorage/bucket_test.go
@@ -6,12 +6,14 @@ package fakestorage
 
 import (
 	"context"
+	"errors"
 	"os"
 	"runtime"
 	"testing"
 	"time"
 
 	"cloud.google.com/go/storage"
+	"google.golang.org/api/googleapi"
 	"google.golang.org/api/iterator"
 )
 
@@ -23,6 +25,26 @@ func tempDir() string {
 	}
 }
 
+func TestServerClientBucketAlreadyExists(t *testing.T) {
+	objs := []Object{
+		{ObjectAttrs: ObjectAttrs{BucketName: "some-bucket", Name: "img/hi-res/party-01.jpg"}},
+	}
+	runServersTest(t, runServersOptions{objs: objs}, func(t *testing.T, server *Server) {
+		client := server.Client()
+		err := client.Bucket("some-bucket").Create(context.Background(), "whatever", nil)
+		if err == nil {
+			t.Errorf("expected a 409 error")
+		}
+		apiErr := new(googleapi.Error)
+		if !errors.As(err, &apiErr) {
+			t.Errorf("expected a google API error, got %T", err)
+		}
+		if apiErr.Code != 409 {
+			t.Errorf("expected a google API error with code 409, got %v", apiErr.Code)
+		}
+	})
+}
+
 func TestServerClientBucketAttrs(t *testing.T) {
 	objs := []Object{
 		{ObjectAttrs: ObjectAttrs{BucketName: "some-bucket", Name: "img/hi-res/party-01.jpg"}},
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index e564c7e..819919a 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -381,34 +381,66 @@ func TestServerClientObjectRangeReader(t *testing.T) {
 
 	runServersTest(t, runServersOptions{objs: objs}, func(t *testing.T, server *Server) {
 		tests := []struct {
-			testCase string
-			offset   int64
-			length   int64
+			testCase     string
+			offset       int64
+			length       int64
+			expectedData string
 		}{
 			{
 				"no length, just offset",
-				2,
+				44,
 				-1,
+				"my object",
 			},
 			{
 				"zero offset, length",
 				0,
-				10,
+				11,
+				"some really",
 			},
 			{
 				"offset and length",
-				4,
+				5,
+				11,
+				"really nice",
+			},
+			{
+				"negative offset",
+				-9,
+				-1,
+				"my object",
+			},
+			{
+				"negative offset before start",
+				-100,
+				-1,
+				content, // Returns all content
+			},
+			{
+				"length too long", // ok
+				44,
+				100,
+				"my object",
+			},
+			{
+				"length too long by exactly one",
+				44,
 				10,
+				"my object",
+			},
+			{
+				"zero range",
+				0,
+				0,
+				// Note: this case is handled by the GCS client, not the
+				// server; the client doesn't pass a range. It receives all the
+				// content, and then returns no content to the caller.
+				"",
 			},
 		}
 		for _, test := range tests {
 			test := test
 			t.Run(test.testCase, func(t *testing.T) {
-				length := test.length
-				if length == -1 {
-					length = int64(len(content)) - test.offset
-				}
-				expectedData := content[test.offset : test.offset+length]
 				client := server.Client()
 				objHandle := client.Bucket(bucketName).Object(objectName)
 				reader, err := objHandle.NewRangeReader(context.TODO(), test.offset, test.length)
@@ -420,8 +452,8 @@ func TestServerClientObjectRangeReader(t *testing.T) {
 				if err != nil {
 					t.Fatal(err)
 				}
-				if string(data) != expectedData {
-					t.Errorf("wrong data returned\nwant %q\ngot  %q", expectedData, string(data))
+				if string(data) != test.expectedData {
+					t.Errorf("wrong data returned\nwant %q\ngot  %q", test.expectedData, string(data))
 				}
 				if ct := reader.Attrs.ContentType; ct != contentType {
 					t.Errorf("wrong content type\nwant %q\ngot  %q", contentType, ct)