@specific.dev/spectest 0.131.6 → 0.131.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,22 @@ uploads, multipart initiation and copies. Upstream currently drops these headers
16
16
  in `metadataHeaders`. The patch applies to the vendored, checksum-verified module;
17
17
  a failed patch stops the build. Recheck it when changing the upstream pin.
18
18
 
19
+ `multipart.patch` adds SHA256 composite multipart integrity to the existing
20
+ uploader: supplied part checksums are validated before storage, completion checks
21
+ each part and its ordering, and the composite checksum survives HEAD/GET for
22
+ reconciliation. Completion conditions use the backend's atomic conditional write.
23
+ Overwriting an object does not inherit the previous object's checksum metadata.
24
+ This patch supports SHA256/COMPOSITE; it does not implement every S3 checksum
25
+ algorithm or streaming checksum trailers. It also supports UploadPartCopy from
26
+ one backend snapshot, including closed byte ranges, exact ETag conditions,
27
+ version selection and SHA256 part receipts. Date-based copy conditions are
28
+ explicitly unsupported (501), rather than silently ignored.
29
+
30
+ Run `./test.sh` from this directory to exercise real HTTP requests against the
31
+ same patched sources used in the image, including corrupt retries, conditional
32
+ completion races, abort, metadata reconciliation and unchecksummed uploads.
33
+ The test runs in a temporary vendor directory and enables Go's race detector.
34
+
19
35
  From `sdk`, run the real-image contract on a Linux Docker host:
20
36
 
21
37
  ```
@@ -11,6 +11,10 @@ import (
11
11
  )
12
12
 
13
13
  func main() {
14
+ log.Fatal(http.ListenAndServe(":9090", newServer()))
15
+ }
16
+
17
+ func newServer() http.Handler {
14
18
  // VM snapshots include RAM: buckets, versions and in-flight multipart state
15
19
  // fork together. A fresh process starts empty, as S3Mock did by default.
16
20
  // Unlike the Bolt backend, s3mem also supports bucket versioning.
@@ -21,7 +25,7 @@ func main() {
21
25
  gofakes3.WithAutoBucket(false),
22
26
  gofakes3.WithLogger(gofakes3.GlobalLog()),
23
27
  ).Server()
24
- log.Fatal(http.ListenAndServe(":9090", cors(server)))
28
+ return cors(server)
25
29
  }
26
30
 
27
31
  // Preserve S3Mock's permissive browser uploads, including arbitrary metadata
@@ -0,0 +1,282 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/sha256"
6
+ "encoding/base64"
7
+ "encoding/xml"
8
+ "fmt"
9
+ "io"
10
+ "net/http"
11
+ "net/http/httptest"
12
+ "strings"
13
+ "sync"
14
+ "testing"
15
+ )
16
+
17
+ type s3Response struct {
18
+ status int
19
+ header http.Header
20
+ body []byte
21
+ }
22
+ type s3Probe struct {
23
+ t *testing.T
24
+ server *httptest.Server
25
+ }
26
+
27
+ func newProbe(t *testing.T) *s3Probe {
28
+ t.Helper()
29
+ p := &s3Probe{t, httptest.NewServer(newServer())}
30
+ t.Cleanup(p.server.Close)
31
+ p.request("PUT", "/uploads", nil, nil, 200)
32
+ return p
33
+ }
34
+ func (p *s3Probe) request(method, path string, body []byte, headers map[string]string, want int) s3Response {
35
+ p.t.Helper()
36
+ req, err := http.NewRequest(method, p.server.URL+path, bytes.NewReader(body))
37
+ if err != nil {
38
+ p.t.Fatal(err)
39
+ }
40
+ for k, v := range headers {
41
+ req.Header.Set(k, v)
42
+ }
43
+ res, err := p.server.Client().Do(req)
44
+ if err != nil {
45
+ p.t.Fatal(err)
46
+ }
47
+ defer res.Body.Close()
48
+ data, err := io.ReadAll(res.Body)
49
+ if err != nil {
50
+ p.t.Fatal(err)
51
+ }
52
+ if res.StatusCode != want {
53
+ p.t.Fatalf("%s %s: got %d want %d: %s", method, path, res.StatusCode, want, data)
54
+ }
55
+ return s3Response{res.StatusCode, res.Header, data}
56
+ }
57
+ func sum(body []byte) string {
58
+ digest := sha256.Sum256(body)
59
+ return base64.StdEncoding.EncodeToString(digest[:])
60
+ }
61
+ func (p *s3Probe) start(key string, checksum bool) string {
62
+ p.t.Helper()
63
+ headers := map[string]string{"Cache-Control": "private, max-age=60"}
64
+ if checksum {
65
+ headers["x-amz-checksum-algorithm"] = "SHA256"
66
+ headers["x-amz-checksum-type"] = "COMPOSITE"
67
+ }
68
+ res := p.request("POST", "/uploads/"+key+"?uploads", nil, headers, 200)
69
+ var out struct {
70
+ UploadID string `xml:"UploadId"`
71
+ }
72
+ if err := xml.Unmarshal(res.body, &out); err != nil || out.UploadID == "" {
73
+ p.t.Fatalf("initiate: %s, %v", res.body, err)
74
+ }
75
+ return "/uploads/" + key + "?uploadId=" + out.UploadID
76
+ }
77
+ func (p *s3Probe) part(path string, number int, body []byte, checksum bool) string {
78
+ p.t.Helper()
79
+ headers := map[string]string{}
80
+ if checksum {
81
+ headers["x-amz-checksum-sha256"] = sum(body)
82
+ }
83
+ res := p.request("PUT", fmt.Sprintf("%s&partNumber=%d", path, number), body, headers, 200)
84
+ if checksum && res.header.Get("x-amz-checksum-sha256") != sum(body) {
85
+ p.t.Fatalf("part checksum acknowledgement: %v", res.header)
86
+ }
87
+ tag := res.header.Get("ETag")
88
+ if tag == "" {
89
+ p.t.Fatal("missing ETag")
90
+ }
91
+ digest := ""
92
+ if checksum {
93
+ digest = "<ChecksumSHA256>" + sum(body) + "</ChecksumSHA256>"
94
+ }
95
+ return fmt.Sprintf("<Part><PartNumber>%d</PartNumber><ETag>%s</ETag>%s</Part>", number, tag, digest)
96
+ }
97
+ func completion(parts ...string) []byte {
98
+ return []byte("<CompleteMultipartUpload>" + strings.Join(parts, "") + "</CompleteMultipartUpload>")
99
+ }
100
+
101
+ func TestMultipartSHA256IntegrityAndReconciliation(t *testing.T) {
102
+ p := newProbe(t)
103
+ path := p.start("large", true)
104
+ first := bytes.Repeat([]byte("a"), 8*1024*1024)
105
+ last := bytes.Repeat([]byte("b"), 1024*1024)
106
+ part1 := p.part(path, 1, first, true)
107
+ // A rejected retry must not replace the already acknowledged part.
108
+ res := p.request("PUT", path+"&partNumber=1", []byte("corrupt"), map[string]string{"x-amz-checksum-sha256": sum(first)}, 400)
109
+ if !bytes.Contains(res.body, []byte("BadDigest")) {
110
+ t.Fatalf("corrupt part: %s", res.body)
111
+ }
112
+ part2 := p.part(path, 2, last, true)
113
+ for _, parts := range [][]string{{part2, part1}, {part1, part1}, {strings.Replace(part1, "<PartNumber>1", "<PartNumber>-1", 1)}, {strings.Replace(part1, sum(first), sum(last), 1), part2}, {strings.Replace(part1, "<ETag>", "<ETag>bad", 1), part2}} {
114
+ p.request("POST", path, completion(parts...), nil, 400)
115
+ }
116
+ p.request("POST", path, completion(part1, part2), map[string]string{"x-amz-checksum-type": "FULL_OBJECT"}, 400)
117
+ p.request("POST", path, completion(part1, part2), map[string]string{"x-amz-checksum-sha256": "wrong"}, 400)
118
+ a, b := sha256.Sum256(first), sha256.Sum256(last)
119
+ expected := sum(append(a[:], b[:]...)) + "-2"
120
+ res = p.request("POST", path, completion(part1, part2), map[string]string{"If-None-Match": "*", "x-amz-checksum-type": "COMPOSITE", "x-amz-checksum-sha256": expected}, 200)
121
+ var completed struct {
122
+ ChecksumSHA256 string
123
+ ChecksumType string
124
+ }
125
+ if err := xml.Unmarshal(res.body, &completed); err != nil {
126
+ t.Fatal(err)
127
+ }
128
+ if completed.ChecksumSHA256 != expected || completed.ChecksumType != "COMPOSITE" {
129
+ t.Fatalf("complete: %s", res.body)
130
+ }
131
+ // Reconcile using retained metadata after an unobserved completion response.
132
+ head := p.request("HEAD", "/uploads/large", nil, map[string]string{"x-amz-checksum-mode": "ENABLED"}, 200)
133
+ if head.header.Get("x-amz-checksum-sha256") != expected || head.header.Get("x-amz-checksum-type") != "COMPOSITE" || head.header.Get("Content-Length") != fmt.Sprint(len(first)+len(last)) || head.header.Get("Cache-Control") != "private, max-age=60" {
134
+ t.Fatalf("reconciliation: %v", head.header)
135
+ }
136
+ got := p.request("GET", "/uploads/large", nil, nil, 200)
137
+ if !bytes.Equal(got.body, append(first, last...)) {
138
+ t.Fatal("completed bytes differ")
139
+ }
140
+ p.request("PUT", path+"&partNumber=1", first, nil, 404)
141
+ // An ordinary overwrite must not inherit an obsolete composite checksum.
142
+ p.request("PUT", "/uploads/large", []byte("replacement"), nil, 200)
143
+ head = p.request("HEAD", "/uploads/large", nil, nil, 200)
144
+ if head.header.Get("x-amz-checksum-sha256") != "" || head.header.Get("x-amz-checksum-type") != "" {
145
+ t.Fatal("overwrite retained stale checksum")
146
+ }
147
+ }
148
+
149
+ func TestMultipartConditionalCompletionIsAtomic(t *testing.T) {
150
+ p := newProbe(t)
151
+ paths := []string{p.start("same", true), p.start("same", true)}
152
+ parts := []string{p.part(paths[0], 1, []byte("first"), true), p.part(paths[1], 1, []byte("second"), true)}
153
+ statuses := make(chan int, 2)
154
+ var wg sync.WaitGroup
155
+ for i := range paths {
156
+ wg.Add(1)
157
+ go func(i int) {
158
+ defer wg.Done()
159
+ req, _ := http.NewRequest("POST", p.server.URL+paths[i], bytes.NewReader(completion(parts[i])))
160
+ req.Header.Set("If-None-Match", "*")
161
+ res, err := p.server.Client().Do(req)
162
+ if err != nil {
163
+ statuses <- 0
164
+ return
165
+ }
166
+ defer res.Body.Close()
167
+ io.Copy(io.Discard, res.Body)
168
+ statuses <- res.StatusCode
169
+ }(i)
170
+ }
171
+ wg.Wait()
172
+ close(statuses)
173
+ counts := map[int]int{}
174
+ for s := range statuses {
175
+ counts[s]++
176
+ }
177
+ if counts[200] != 1 || counts[412] != 1 {
178
+ t.Fatalf("competing creates: %v", counts)
179
+ }
180
+ body := p.request("GET", "/uploads/same", nil, nil, 200).body
181
+ if string(body) != "first" && string(body) != "second" {
182
+ t.Fatalf("bad winner: %s", body)
183
+ }
184
+ }
185
+
186
+ func TestMultipartAbortAndLegacyUpload(t *testing.T) {
187
+ p := newProbe(t)
188
+ path := p.start("aborted", true)
189
+ part := p.part(path, 1, []byte("abc"), true)
190
+ p.request("DELETE", path, nil, nil, 204)
191
+ p.request("POST", path, completion(part), nil, 404)
192
+ p.request("HEAD", "/uploads/aborted", nil, nil, 404)
193
+ path = p.start("legacy", false)
194
+ part = p.part(path, 1, []byte("legacy"), false)
195
+ p.request("POST", path, completion(part), nil, 200)
196
+ if got := p.request("GET", "/uploads/legacy", nil, nil, 200); string(got.body) != "legacy" {
197
+ t.Fatal("legacy multipart changed")
198
+ }
199
+ }
200
+
201
+ func TestUploadPartCopyRetainsExactSourceAndRejectsStaleRetry(t *testing.T) {
202
+ p := newProbe(t)
203
+ source := bytes.Repeat([]byte("0123456789"), 600000)
204
+ put := p.request("PUT", "/uploads/staging%20%2B%25.csv", source, nil, 200)
205
+ path := p.start("copied", false)
206
+ headers := map[string]string{"x-amz-copy-source": "/uploads/staging%20%2B%25.csv", "x-amz-copy-source-if-match": put.header.Get("ETag"), "x-amz-copy-source-range": "bytes=3-5242882"}
207
+ res := p.request("PUT", path+"&partNumber=1", nil, headers, 200)
208
+ var part struct {
209
+ XMLName xml.Name
210
+ ETag string
211
+ LastModified string
212
+ }
213
+ if err := xml.Unmarshal(res.body, &part); err != nil || part.XMLName.Local != "CopyPartResult" || part.ETag == "" || part.LastModified == "" {
214
+ t.Fatalf("copy part receipt: %s: %v", res.body, err)
215
+ }
216
+ first := fmt.Sprintf("<Part><PartNumber>1</PartNumber><ETag>%s</ETag></Part>", part.ETag)
217
+ headers["x-amz-copy-source-if-match"] = `"stale"`
218
+ p.request("PUT", path+"&partNumber=1", nil, headers, 412)
219
+ headers["x-amz-copy-source-if-match"] = put.header.Get("ETag")
220
+ for _, invalid := range []string{"bytes=6000000-6000001", "bytes=3-6000000", "bytes=5-3", "bytes=-3", "bytes=3-", "bytes=0-1,3-4"} {
221
+ headers["x-amz-copy-source-range"] = invalid
222
+ p.request("PUT", path+"&partNumber=1", nil, headers, 416)
223
+ }
224
+ headers["x-amz-copy-source-range"] = "bytes=5242883-5999999"
225
+ res = p.request("PUT", path+"&partNumber=2", nil, headers, 200)
226
+ if err := xml.Unmarshal(res.body, &part); err != nil {
227
+ t.Fatal(err)
228
+ }
229
+ second := fmt.Sprintf("<Part><PartNumber>2</PartNumber><ETag>%s</ETag></Part>", part.ETag)
230
+ p.request("POST", path, completion(first, second), nil, 200)
231
+ got := p.request("GET", "/uploads/copied", nil, nil, 200)
232
+ if !bytes.Equal(got.body, source[3:]) {
233
+ t.Fatal("copy changed the retained byte range")
234
+ }
235
+ p.request("PUT", path+"&partNumber=2", nil, headers, 404)
236
+ }
237
+
238
+ func TestUploadPartCopyVersionChecksumAndAbort(t *testing.T) {
239
+ p := newProbe(t)
240
+ p.request("PUT", "/uploads?versioning", []byte(`<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>`), nil, 200)
241
+ original := []byte("retained original version")
242
+ source := p.request("PUT", "/uploads/versioned", original, nil, 200)
243
+ version := source.header.Get("x-amz-version-id")
244
+ if version == "" {
245
+ t.Fatal("source has no version")
246
+ }
247
+ p.request("PUT", "/uploads/versioned", []byte("replacement"), nil, 200)
248
+ path := p.start("version-copy", true)
249
+ headers := map[string]string{"x-amz-copy-source": "/uploads/versioned?versionId=" + version, "x-amz-copy-source-if-match": source.header.Get("ETag")}
250
+ res := p.request("PUT", path+"&partNumber=1", nil, headers, 200)
251
+ var part struct {
252
+ ETag string
253
+ ChecksumSHA256 string
254
+ }
255
+ if err := xml.Unmarshal(res.body, &part); err != nil {
256
+ t.Fatal(err)
257
+ }
258
+ if part.ChecksumSHA256 != sum(original) {
259
+ t.Fatal("copied version checksum differs")
260
+ }
261
+ headers["x-amz-copy-source-if-none-match"] = source.header.Get("ETag")
262
+ p.request("PUT", path+"&partNumber=1", nil, headers, 412)
263
+ delete(headers, "x-amz-copy-source-if-none-match")
264
+ headers["x-amz-copy-source-if-modified-since"] = "Sat, 31 Oct 2026 00:00:00 GMT"
265
+ p.request("PUT", path+"&partNumber=1", nil, headers, 501)
266
+ delete(headers, "x-amz-copy-source-if-modified-since")
267
+ descriptor := fmt.Sprintf("<Part><PartNumber>1</PartNumber><ETag>%s</ETag><ChecksumSHA256>%s</ChecksumSHA256></Part>", part.ETag, part.ChecksumSHA256)
268
+ digest := sha256.Sum256(original)
269
+ composite := sum(digest[:]) + "-1"
270
+ p.request("POST", path, completion(descriptor), map[string]string{"x-amz-checksum-type": "COMPOSITE", "x-amz-checksum-sha256": composite}, 200)
271
+ got := p.request("GET", "/uploads/version-copy", nil, nil, 200)
272
+ if !bytes.Equal(got.body, original) || got.header.Get("x-amz-checksum-sha256") != composite {
273
+ t.Fatal("versioned copy bytes or composite receipt changed")
274
+ }
275
+ path = p.start("aborted-copy", false)
276
+ p.request("DELETE", path, nil, nil, 204)
277
+ p.request("PUT", path+"&partNumber=1", nil, headers, 404)
278
+ headers["x-amz-copy-source"] = "/uploads/missing"
279
+ path = p.start("missing-copy", false)
280
+ p.request("PUT", path+"&partNumber=1", nil, headers, 404)
281
+ p.request("DELETE", path, nil, nil, 204)
282
+ }
@@ -0,0 +1,352 @@
1
+ --- a/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
2
+ +++ b/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
3
+ @@ -933,6 +933,14 @@
4
+ return err
5
+ }
6
+
7
+ + if meta["X-Amz-Checksum-Algorithm"] == "SHA256" {
8
+ + if kind := meta["X-Amz-Checksum-Type"]; kind != "" && kind != "COMPOSITE" {
9
+ + return ErrInvalidArgument
10
+ + }
11
+ + meta["X-Amz-Checksum-Type"] = "COMPOSITE"
12
+ + w.Header().Set("x-amz-checksum-algorithm", "SHA256")
13
+ + w.Header().Set("x-amz-checksum-type", "COMPOSITE")
14
+ + }
15
+ uploadID, err := g.uploader.CreateMultipartUpload(bucket, object, meta)
16
+ if err != nil {
17
+ return err
18
+ @@ -983,12 +991,20 @@
19
+ }
20
+ }
21
+
22
+ + var checksum *sha256PartReader
23
+ + if expected := r.Header.Get("x-amz-checksum-sha256"); expected != "" {
24
+ + checksum = newSHA256PartReader(rdr, expected)
25
+ + rdr = checksum
26
+ + }
27
+ etag, err := g.uploader.UploadPart(bucket, object, uploadID, int(partNumber), r.ContentLength, rdr)
28
+ if err != nil {
29
+ return err
30
+ }
31
+
32
+ w.Header().Add("ETag", etag)
33
+ + if checksum != nil {
34
+ + w.Header().Set("x-amz-checksum-sha256", checksum.sum())
35
+ + }
36
+ return nil
37
+ }
38
+
39
+ @@ -1009,6 +1025,13 @@
40
+ return err
41
+ }
42
+
43
+ + var err error
44
+ + in.Conditions, err = parsePutConditions(r.Header)
45
+ + if err != nil {
46
+ + return err
47
+ + }
48
+ + in.ChecksumSHA256 = r.Header.Get("x-amz-checksum-sha256")
49
+ + in.ChecksumType = r.Header.Get("x-amz-checksum-type")
50
+ versionID, etag, err := g.uploader.CompleteMultipartUpload(bucket, object, uploadID, &in)
51
+ if err != nil {
52
+ return err
53
+ @@ -1031,10 +1054,12 @@
54
+ }
55
+
56
+ return g.xmlEncoder(w).Encode(&CompleteMultipartUploadResult{
57
+ - ETag: etag,
58
+ - Bucket: bucket,
59
+ - Key: object,
60
+ - Location: location,
61
+ + ETag: etag,
62
+ + Bucket: bucket,
63
+ + Key: object,
64
+ + Location: location,
65
+ + ChecksumSHA256: in.ChecksumSHA256,
66
+ + ChecksumType: in.ChecksumType,
67
+ })
68
+ }
69
+
70
+ --- a/vendor/github.com/johannesboyne/gofakes3/uploader.go
71
+ +++ b/vendor/github.com/johannesboyne/gofakes3/uploader.go
72
+ @@ -3,6 +3,8 @@
73
+ import (
74
+ "bytes"
75
+ "crypto/md5"
76
+ + "crypto/sha256"
77
+ + "encoding/base64"
78
+ "encoding/hex"
79
+ "fmt"
80
+ "io"
81
+ @@ -428,13 +430,26 @@
82
+ return "", "", ErrInvalidPart
83
+ }
84
+
85
+ - if !input.partsAreSorted() {
86
+ + if len(input.Parts) == 0 || !input.partsAreSorted() {
87
+ return "", "", ErrInvalidPartOrder
88
+ }
89
+
90
+ var size int64
91
+
92
+ - for _, inPart := range input.Parts {
93
+ + composite := sha256.New()
94
+ + withSHA256 := mpu.Meta["X-Amz-Checksum-Algorithm"] == "SHA256"
95
+ + if withSHA256 && input.ChecksumType != "" && input.ChecksumType != "COMPOSITE" {
96
+ + return "", "", ErrBadDigest
97
+ + }
98
+ + previous := 0
99
+ + for i, inPart := range input.Parts {
100
+ + if inPart.PartNumber <= previous {
101
+ + return "", "", ErrInvalidPartOrder
102
+ + }
103
+ + previous = inPart.PartNumber
104
+ + if withSHA256 && inPart.PartNumber != i+1 {
105
+ + return "", "", ErrInvalidPartOrder
106
+ + }
107
+ if inPart.PartNumber >= mpuPartsLen || mpu.parts[inPart.PartNumber] == nil {
108
+ return "", "", ErrorMessagef(ErrInvalidPart, "unexpected part number %d in complete request", inPart.PartNumber)
109
+ }
110
+ @@ -444,6 +459,13 @@
111
+ return "", "", ErrorMessagef(ErrInvalidPart, "unexpected part etag for number %d in complete request", inPart.PartNumber)
112
+ }
113
+
114
+ + if withSHA256 {
115
+ + digest := sha256.Sum256(upPart.Body)
116
+ + if inPart.ChecksumSHA256 != base64.StdEncoding.EncodeToString(digest[:]) {
117
+ + return "", "", ErrBadDigest
118
+ + }
119
+ + composite.Write(digest[:])
120
+ + }
121
+ size += int64(len(upPart.Body))
122
+ }
123
+
124
+ @@ -461,7 +483,20 @@
125
+
126
+ etag = fmt.Sprintf(`"%s-%d"`, hex.EncodeToString(hash.Sum(nil)), len(input.Parts))
127
+
128
+ - result, err := u.storage.PutObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), nil)
129
+ + if withSHA256 {
130
+ + checksum := fmt.Sprintf("%s-%d", base64.StdEncoding.EncodeToString(composite.Sum(nil)), len(input.Parts))
131
+ + if input.ChecksumSHA256 != "" && input.ChecksumSHA256 != checksum {
132
+ + return "", "", ErrBadDigest
133
+ + }
134
+ + input.ChecksumSHA256 = checksum
135
+ + input.ChecksumType = "COMPOSITE"
136
+ + mpu.Meta["X-Amz-Checksum-Sha256"] = checksum
137
+ + mpu.Meta["X-Amz-Checksum-Type"] = "COMPOSITE"
138
+ + } else {
139
+ + input.ChecksumSHA256 = ""
140
+ + input.ChecksumType = ""
141
+ + }
142
+ + result, err := u.storage.PutObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), input.Conditions)
143
+ if err != nil {
144
+ return "", "", err
145
+ }
146
+ --- a/vendor/github.com/johannesboyne/gofakes3/messages.go
147
+ +++ b/vendor/github.com/johannesboyne/gofakes3/messages.go
148
+ @@ -48,12 +48,16 @@
149
+ }
150
+
151
+ type CompletedPart struct {
152
+ - PartNumber int `xml:"PartNumber"`
153
+ - ETag string `xml:"ETag"`
154
+ + ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
155
+ + PartNumber int `xml:"PartNumber"`
156
+ + ETag string `xml:"ETag"`
157
+ }
158
+
159
+ type CompleteMultipartUploadRequest struct {
160
+ - Parts []CompletedPart `xml:"Part"`
161
+ + Conditions *PutConditions `xml:"-"`
162
+ + ChecksumSHA256 string `xml:"-"`
163
+ + ChecksumType string `xml:"-"`
164
+ + Parts []CompletedPart `xml:"Part"`
165
+ }
166
+
167
+ func (c CompleteMultipartUploadRequest) partsAreSorted() bool {
168
+ @@ -70,10 +74,12 @@
169
+ }
170
+
171
+ type CompleteMultipartUploadResult struct {
172
+ - Location string `xml:"Location"`
173
+ - Bucket string `xml:"Bucket"`
174
+ - Key string `xml:"Key"`
175
+ - ETag string `xml:"ETag"`
176
+ + ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
177
+ + ChecksumType string `xml:"ChecksumType,omitempty"`
178
+ + Location string `xml:"Location"`
179
+ + Bucket string `xml:"Bucket"`
180
+ + Key string `xml:"Key"`
181
+ + ETag string `xml:"ETag"`
182
+ }
183
+
184
+ type Content struct {
185
+ --- a/vendor/github.com/johannesboyne/gofakes3/checksum.go
186
+ +++ b/vendor/github.com/johannesboyne/gofakes3/checksum.go
187
+ @@ -0,0 +1,32 @@
188
+ +package gofakes3
189
+ +
190
+ +import (
191
+ + "crypto/sha256"
192
+ + "encoding/base64"
193
+ + "hash"
194
+ + "io"
195
+ +)
196
+ +
197
+ +// Validate before UploadPart commits bytes to the existing multipart store.
198
+ +type sha256PartReader struct {
199
+ + input io.Reader
200
+ + digest hash.Hash
201
+ + expected string
202
+ +}
203
+ +
204
+ +func newSHA256PartReader(input io.Reader, expected string) *sha256PartReader {
205
+ + return &sha256PartReader{input: input, digest: sha256.New(), expected: expected}
206
+ +}
207
+ +
208
+ +func (r *sha256PartReader) sum() string {
209
+ + return base64.StdEncoding.EncodeToString(r.digest.Sum(nil))
210
+ +}
211
+ +
212
+ +func (r *sha256PartReader) Read(p []byte) (int, error) {
213
+ + n, err := r.input.Read(p)
214
+ + r.digest.Write(p[:n])
215
+ + if err == io.EOF && r.sum() != r.expected {
216
+ + return n, ErrBadDigest
217
+ + }
218
+ + return n, err
219
+ +}
220
+ --- a/vendor/github.com/johannesboyne/gofakes3/backend.go
221
+ +++ b/vendor/github.com/johannesboyne/gofakes3/backend.go
222
+ @@ -3,6 +3,7 @@
223
+ import (
224
+ "encoding/hex"
225
+ "errors"
226
+ + "strings"
227
+
228
+ "io"
229
+ "time"
230
+ @@ -434,6 +435,11 @@
231
+ // carry over metadata if it exists
232
+ if existingObj != nil {
233
+ for k, v := range existingObj.Metadata {
234
+ + // Integrity metadata belongs to the bytes being written, never a
235
+ + // previous version of this key. CopyObject supplies its own metadata.
236
+ + if strings.HasPrefix(k, "X-Amz-Checksum-") {
237
+ + continue
238
+ + }
239
+ // new metadata overwrites old but keep the rest
240
+ // TODO: check how metadata can be deleted?!
241
+ if _, ok := meta[k]; !ok {
242
+ --- a/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
243
+ +++ b/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
244
+ @@ -960,6 +960,10 @@
245
+ return ErrInvalidPart
246
+ }
247
+
248
+ + if r.Header.Get("x-amz-copy-source") != "" {
249
+ + return g.copyMultipartUploadPart(bucket, object, uploadID, int(partNumber), w, r)
250
+ + }
251
+ +
252
+ size, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
253
+ if err != nil || size <= 0 {
254
+ return ErrMissingContentLength
255
+ --- /dev/null
256
+ +++ b/vendor/github.com/johannesboyne/gofakes3/upload_part_copy.go
257
+ @@ -0,0 +1,95 @@
258
+ +package gofakes3
259
+ +
260
+ +import (
261
+ + "crypto/sha256"
262
+ + "encoding/base64"
263
+ + "encoding/xml"
264
+ + "io"
265
+ + "net/http"
266
+ + "net/url"
267
+ + "strings"
268
+ +)
269
+ +
270
+ +// Copy from one backend snapshot: a separate HEAD followed by GET could race
271
+ +// an overwrite and copy bytes that no longer match the caller's condition.
272
+ +func (g *GoFakeS3) copyMultipartUploadPart(bucket, object string, uploadID UploadID, number int, w http.ResponseWriter, r *http.Request) error {
273
+ + source, query, _ := strings.Cut(r.Header.Get("x-amz-copy-source"), "?")
274
+ + source, err := url.PathUnescape(source)
275
+ + if err != nil {
276
+ + return ErrInvalidArgument
277
+ + }
278
+ + parts := strings.SplitN(strings.TrimPrefix(source, "/"), "/", 2)
279
+ + if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
280
+ + return ErrInvalidArgument
281
+ + }
282
+ + params, err := url.ParseQuery(query)
283
+ + if err != nil {
284
+ + return ErrInvalidArgument
285
+ + }
286
+ + for key := range params {
287
+ + if key != "versionId" {
288
+ + return ErrInvalidArgument
289
+ + }
290
+ + }
291
+ + // Do not silently ignore conditions this compatibility subset cannot apply.
292
+ + for _, key := range []string{"x-amz-copy-source-if-modified-since", "x-amz-copy-source-if-unmodified-since"} {
293
+ + if r.Header.Get(key) != "" {
294
+ + return ErrNotImplemented
295
+ + }
296
+ + }
297
+ + var src *Object
298
+ + if version := params.Get("versionId"); version != "" {
299
+ + if g.versioned == nil {
300
+ + return ErrNotImplemented
301
+ + }
302
+ + src, err = g.versioned.GetObjectVersion(parts[0], parts[1], VersionID(version), nil)
303
+ + } else {
304
+ + src, err = g.storage.GetObject(parts[0], parts[1], nil)
305
+ + }
306
+ + if err != nil {
307
+ + return err
308
+ + }
309
+ + if src == nil || src.Contents == nil {
310
+ + return ErrInternal
311
+ + }
312
+ + defer src.Contents.Close()
313
+ + if src.IsDeleteMarker {
314
+ + return ErrInvalidArgument
315
+ + }
316
+ + tag := FormatETag(src.Hash)
317
+ + if expected := r.Header.Get("x-amz-copy-source-if-match"); expected != "" && expected != tag {
318
+ + return ErrPreconditionFailed
319
+ + }
320
+ + if expected := r.Header.Get("x-amz-copy-source-if-none-match"); expected != "" && expected == tag {
321
+ + return ErrPreconditionFailed
322
+ + }
323
+ + start, size := int64(0), src.Size
324
+ + if value := r.Header.Get("x-amz-copy-source-range"); value != "" {
325
+ + span, err := parseRangeHeader(value)
326
+ + if err != nil || span == nil || span.FromEnd || span.End == RangeNoEnd || span.End >= src.Size {
327
+ + return ErrInvalidRange
328
+ + }
329
+ + start, size = span.Start, span.End-span.Start+1
330
+ + }
331
+ + if size <= 0 {
332
+ + return ErrInvalidRange
333
+ + }
334
+ + if _, err := io.CopyN(io.Discard, src.Contents, start); err != nil {
335
+ + return err
336
+ + }
337
+ + digest := sha256.New()
338
+ + body := io.TeeReader(io.LimitReader(src.Contents, size), digest)
339
+ + etag, err := g.uploader.UploadPart(bucket, object, uploadID, number, size, body)
340
+ + if err != nil {
341
+ + return err
342
+ + }
343
+ + if src.VersionID != "" {
344
+ + w.Header().Set("x-amz-copy-source-version-id", string(src.VersionID))
345
+ + }
346
+ + return g.xmlEncoder(w).Encode(struct {
347
+ + XMLName xml.Name `xml:"CopyPartResult"`
348
+ + ETag string `xml:"ETag"`
349
+ + LastModified ContentTime `xml:"LastModified"`
350
+ + ChecksumSHA256 string `xml:"ChecksumSHA256"`
351
+ + }{ETag: etag, LastModified: NewContentTime(g.timeSource.Now()), ChecksumSHA256: base64.StdEncoding.EncodeToString(digest.Sum(nil))})
352
+ +}
@@ -0,0 +1,12 @@
1
+ #!/bin/sh
2
+ # Exercise the exact vendored compatibility patches used by the runtime image.
3
+ set -eu
4
+ source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
+ test_dir=$(mktemp -d)
6
+ trap 'rm -rf "$test_dir"' EXIT HUP INT TERM
7
+ cp "$source_dir"/*.go "$source_dir/go.mod" "$source_dir/go.sum" "$source_dir"/*.patch "$test_dir/"
8
+ cd "$test_dir"
9
+ go mod vendor
10
+ patch -p1 < metadata.patch
11
+ patch -p1 < multipart.patch
12
+ go test -mod=vendor -race ./...