1package gofakes3
2
3import (
4	"encoding/xml"
5	"fmt"
6	"net/http"
7	"time"
8)
9
10// Error codes are documented here:
11// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
12//
13// If you add a code to this list, please also add it to ErrorCode.Status().
14//
15const (
16	ErrNone ErrorCode = ""
17
18	// The Content-MD5 you specified did not match what we received.
19	ErrBadDigest ErrorCode = "BadDigest"
20
21	ErrBucketAlreadyExists ErrorCode = "BucketAlreadyExists"
22
23	// Raised when attempting to delete a bucket that still contains items.
24	ErrBucketNotEmpty ErrorCode = "BucketNotEmpty"
25
26	// "Indicates that the versioning configuration specified in the request is invalid"
27	ErrIllegalVersioningConfiguration ErrorCode = "IllegalVersioningConfigurationException"
28
29	// You did not provide the number of bytes specified by the Content-Length
30	// HTTP header:
31	ErrIncompleteBody ErrorCode = "IncompleteBody"
32
33	// POST requires exactly one file upload per request.
34	ErrIncorrectNumberOfFilesInPostRequest ErrorCode = "IncorrectNumberOfFilesInPostRequest"
35
36	// InlineDataTooLarge occurs when using the PutObjectInline method of the
37	// SOAP interface
38	// (https://docs.aws.amazon.com/AmazonS3/latest/API/SOAPPutObjectInline.html).
39	// This is not documented on the errors page; the error is included here
40	// only for reference.
41	ErrInlineDataTooLarge ErrorCode = "InlineDataTooLarge"
42
43	ErrInvalidArgument ErrorCode = "InvalidArgument"
44
45	// https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules
46	ErrInvalidBucketName ErrorCode = "InvalidBucketName"
47
48	// The Content-MD5 you specified is not valid.
49	ErrInvalidDigest ErrorCode = "InvalidDigest"
50
51	ErrInvalidRange         ErrorCode = "InvalidRange"
52	ErrInvalidToken         ErrorCode = "InvalidToken"
53	ErrKeyTooLong           ErrorCode = "KeyTooLongError" // This is not a typo: Error is part of the string, but redundant in the constant name
54	ErrMalformedPOSTRequest ErrorCode = "MalformedPOSTRequest"
55
56	// One or more of the specified parts could not be found. The part might
57	// not have been uploaded, or the specified entity tag might not have
58	// matched the part's entity tag.
59	ErrInvalidPart ErrorCode = "InvalidPart"
60
61	// The list of parts was not in ascending order. Parts list must be
62	// specified in order by part number.
63	ErrInvalidPartOrder ErrorCode = "InvalidPartOrder"
64
65	ErrInvalidURI ErrorCode = "InvalidURI"
66
67	ErrMetadataTooLarge ErrorCode = "MetadataTooLarge"
68	ErrMethodNotAllowed ErrorCode = "MethodNotAllowed"
69	ErrMalformedXML     ErrorCode = "MalformedXML"
70
71	// You must provide the Content-Length HTTP header.
72	ErrMissingContentLength ErrorCode = "MissingContentLength"
73
74	// See BucketNotFound() for a helper function for this error:
75	ErrNoSuchBucket ErrorCode = "NoSuchBucket"
76
77	// See KeyNotFound() for a helper function for this error:
78	ErrNoSuchKey ErrorCode = "NoSuchKey"
79
80	// The specified multipart upload does not exist. The upload ID might be
81	// invalid, or the multipart upload might have been aborted or completed.
82	ErrNoSuchUpload ErrorCode = "NoSuchUpload"
83
84	ErrNoSuchVersion ErrorCode = "NoSuchVersion"
85
86	ErrRequestTimeTooSkewed ErrorCode = "RequestTimeTooSkewed"
87	ErrTooManyBuckets       ErrorCode = "TooManyBuckets"
88	ErrNotImplemented       ErrorCode = "NotImplemented"
89
90	ErrInternal ErrorCode = "InternalError"
91)
92
93// INTERNAL errors! These are not part of the S3 interface, they are codes
94// we have declared ourselves. Should all map to a 500 status code:
95const (
96	ErrInternalPageNotImplemented InternalErrorCode = "PaginationNotImplemented"
97)
98
99// errorResponse should be implemented by any type that needs to be handled by
100// ensureErrorResponse.
101type errorResponse interface {
102	Error
103	enrich(requestID string)
104}
105
106func ensureErrorResponse(err error, requestID string) Error {
107	switch err := err.(type) {
108	case errorResponse:
109		err.enrich(requestID)
110		return err
111
112	case ErrorCode:
113		return &ErrorResponse{
114			Code:      err,
115			RequestID: requestID,
116			Message:   string(err),
117		}
118
119	default:
120		return &ErrorResponse{
121			Code:      ErrInternal,
122			Message:   "Internal Error",
123			RequestID: requestID,
124		}
125	}
126}
127
128type Error interface {
129	error
130	ErrorCode() ErrorCode
131}
132
133// ErrorResponse is the base error type returned by S3 when any error occurs.
134//
135// Some errors contain their own additional fields in the response, for example
136// ErrRequestTimeTooSkewed, which contains the server time and the skew limit.
137// To create one of these responses, subclass it (but please don't export it):
138//
139//	type notQuiteRightResponse struct {
140//		ErrorResponse
141//		ExtraField int
142//	}
143//
144// Next, create a constructor that populates the error. Interfaces won't work
145// for this job as the error itself does double-duty as the XML response
146// object. Fill the struct out however you please, but don't forget to assign
147// Code and Message:
148//
149//	func NotQuiteRight(at time.Time, max time.Duration) error {
150// 	    code := ErrNotQuiteRight
151// 	    return &notQuiteRightResponse{
152// 	        ErrorResponse{Code: code, Message: code.Message()},
153// 	        123456789,
154// 	    }
155// 	}
156//
157type ErrorResponse struct {
158	XMLName xml.Name `xml:"Error"`
159
160	Code      ErrorCode
161	Message   string `xml:",omitempty"`
162	RequestID string `xml:"RequestId,omitempty"`
163	HostID    string `xml:"HostId,omitempty"`
164}
165
166func (e *ErrorResponse) ErrorCode() ErrorCode { return e.Code }
167
168func (e *ErrorResponse) Error() string {
169	return fmt.Sprintf("%s: %s", e.Code, e.Message)
170}
171
172func (r *ErrorResponse) enrich(requestID string) {
173	r.RequestID = requestID
174}
175
176func ErrorMessage(code ErrorCode, message string) error {
177	return &ErrorResponse{Code: code, Message: message}
178}
179
180func ErrorMessagef(code ErrorCode, message string, args ...interface{}) error {
181	return &ErrorResponse{Code: code, Message: fmt.Sprintf(message, args...)}
182}
183
184type ErrorInvalidArgumentResponse struct {
185	ErrorResponse
186
187	ArgumentName  string `xml:"ArgumentName"`
188	ArgumentValue string `xml:"ArgumentValue"`
189}
190
191func ErrorInvalidArgument(name, value, message string) error {
192	return &ErrorInvalidArgumentResponse{
193		ErrorResponse: ErrorResponse{Code: ErrInvalidArgument, Message: message},
194		ArgumentName:  name, ArgumentValue: value}
195}
196
197// ErrorCode represents an S3 error code, documented here:
198// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
199type ErrorCode string
200
201func (e ErrorCode) ErrorCode() ErrorCode { return e }
202func (e ErrorCode) Error() string        { return string(e) }
203
204// InternalErrorCode represents an GoFakeS3 error code. It maps to ErrInternal
205// when constructing a response.
206type InternalErrorCode string
207
208func (e InternalErrorCode) ErrorCode() ErrorCode { return ErrInternal }
209func (e InternalErrorCode) Error() string        { return string(ErrInternal) }
210
211// Message tries to return the same string as S3 would return for the error
212// response, when it is known, or nothing when it is not. If you see the status
213// text for a code we don't have listed in here in the wild, please let us
214// know!
215func (e ErrorCode) Message() string {
216	switch e {
217	case ErrInvalidBucketName:
218		return `Bucket name must match the regex "^[a-zA-Z0-9.\-_]{1,255}$"`
219	case ErrNoSuchBucket:
220		return "The specified bucket does not exist"
221	case ErrRequestTimeTooSkewed:
222		return "The difference between the request time and the current time is too large"
223	case ErrMalformedXML:
224		return "The XML you provided was not well-formed or did not validate against our published schema"
225	default:
226		return ""
227	}
228}
229
230func (e ErrorCode) Status() int {
231	switch e {
232	case ErrBucketAlreadyExists,
233		ErrBucketNotEmpty:
234		return http.StatusConflict
235
236	case ErrBadDigest,
237		ErrIllegalVersioningConfiguration,
238		ErrIncompleteBody,
239		ErrIncorrectNumberOfFilesInPostRequest,
240		ErrInlineDataTooLarge,
241		ErrInvalidArgument,
242		ErrInvalidBucketName,
243		ErrInvalidDigest,
244		ErrInvalidPart,
245		ErrInvalidPartOrder,
246		ErrInvalidToken,
247		ErrInvalidURI,
248		ErrKeyTooLong,
249		ErrMetadataTooLarge,
250		ErrMethodNotAllowed,
251		ErrMalformedPOSTRequest,
252		ErrMalformedXML,
253		ErrTooManyBuckets:
254		return http.StatusBadRequest
255
256	case ErrRequestTimeTooSkewed:
257		return http.StatusForbidden
258
259	case ErrInvalidRange:
260		return http.StatusRequestedRangeNotSatisfiable
261
262	case ErrNoSuchBucket,
263		ErrNoSuchKey,
264		ErrNoSuchUpload,
265		ErrNoSuchVersion:
266		return http.StatusNotFound
267
268	case ErrNotImplemented:
269		return http.StatusNotImplemented
270
271	case ErrMissingContentLength:
272		return http.StatusLengthRequired
273
274	case ErrInternal:
275		return http.StatusInternalServerError
276	}
277
278	return http.StatusInternalServerError
279}
280
281// HasErrorCode asserts that the error has a specific error code:
282//
283//	if HasErrorCode(err, ErrNoSuchBucket) {
284//		// handle condition
285//	}
286//
287// If err is nil and code is ErrNone, HasErrorCode returns true.
288//
289func HasErrorCode(err error, code ErrorCode) bool {
290	if err == nil && code == "" {
291		return true
292	}
293	s3err, ok := err.(interface{ ErrorCode() ErrorCode })
294	if !ok {
295		return false
296	}
297	return s3err.ErrorCode() == code
298}
299
300// IsAlreadyExists asserts that the error is a kind that indicates the resource
301// already exists, similar to os.IsExist.
302func IsAlreadyExists(err error) bool {
303	return HasErrorCode(err, ErrBucketAlreadyExists)
304}
305
306type resourceErrorResponse struct {
307	ErrorResponse
308	Resource string
309}
310
311var _ errorResponse = &resourceErrorResponse{}
312
313func ResourceError(code ErrorCode, resource string) error {
314	return &resourceErrorResponse{
315		ErrorResponse{Code: code, Message: code.Message()},
316		resource,
317	}
318}
319
320func BucketNotFound(bucket string) error { return ResourceError(ErrNoSuchBucket, bucket) }
321func KeyNotFound(key string) error       { return ResourceError(ErrNoSuchKey, key) }
322
323type requestTimeTooSkewedResponse struct {
324	ErrorResponse
325	ServerTime                 time.Time
326	MaxAllowedSkewMilliseconds durationAsMilliseconds
327}
328
329var _ errorResponse = &requestTimeTooSkewedResponse{}
330
331func requestTimeTooSkewed(at time.Time, max time.Duration) error {
332	code := ErrRequestTimeTooSkewed
333	return &requestTimeTooSkewedResponse{
334		ErrorResponse{Code: code, Message: code.Message()},
335		at, durationAsMilliseconds(max),
336	}
337}
338
339// durationAsMilliseconds tricks xml.Marsha into serialising a time.Duration as
340// truncated milliseconds instead of nanoseconds.
341type durationAsMilliseconds time.Duration
342
343func (m durationAsMilliseconds) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
344	var s = fmt.Sprintf("%d", time.Duration(m)/time.Millisecond)
345	return e.EncodeElement(s, start)
346}
347