package httpbinding import ( "encoding/base64" "fmt" "math/big" "net/http" "strconv" "strings" "time" "github.com/aws/smithy-go" "github.com/aws/smithy-go/document" "github.com/aws/smithy-go/traits" smithyhttp "github.com/aws/smithy-go/transport/http" ) // ShapeDeserializer reads HTTP-bound output struct members from the response, // delegates body members to the wrapped body deserializer. type ShapeDeserializer struct { response *http.Response body smithy.ShapeDeserializer payload []byte // ALL http binding traits are applied on the top-level output struct, for // anything nested we are just delegating to the payload deserializer depth int topLevel *smithy.Schema // unlike an RPC-style protocol, members of the top-level output could be // HTTP-bound, so we track that when ReadStruct is first called and "yield" // them back to the caller through ReadStructMember inBindings bool bindings []*smithy.Schema bindingIndex int inBody bool hasPayload bool inHeaderList bool headerListValues []string headerListIdx int inPrefixMap bool prefixValue string prefixKeys []string prefixKeyIdx int } var _ smithy.ShapeDeserializer = (*ShapeDeserializer)(nil) // ShapeDeserializerOptions configures ShapeDeserializer. type ShapeDeserializerOptions struct{} // NewShapeDeserializer creates a ShapeDeserializer for the given HTTP // response. // // The payload should be nil in streaming-blob response operations. func NewShapeDeserializer(resp *http.Response, body smithy.ShapeDeserializer, payload []byte, opts ...func(*ShapeDeserializerOptions)) *ShapeDeserializer { return &ShapeDeserializer{ response: resp, body: body, payload: payload, } } // ReadStruct implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadStruct(s *smithy.Schema) error { d.depth++ if d.depth > 1 { return d.body.ReadStruct(s) } d.topLevel = s for _, member := range s.Members() { if _, ok := smithy.SchemaTrait[*traits.HTTPPayload](member); ok { d.hasPayload = true } if d.isBindingSet(member) { d.bindings = append(d.bindings, member) } } return nil } // ReadStructMember implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadStructMember() (*smithy.Schema, error) { if d.depth > 1 { ms, err := d.body.ReadStructMember() if ms == nil { d.depth-- } return ms, err } if d.bindingIndex < len(d.bindings) { member := d.bindings[d.bindingIndex] d.bindingIndex++ d.inBindings = true return member, nil } d.inBindings = false if d.hasPayload { // i.e. no unbound members d.depth-- return nil, nil } if !d.inBody { d.inBody = true if err := d.body.ReadStruct(d.topLevel); err != nil { return nil, err } } ms, err := d.body.ReadStructMember() if ms == nil { d.depth-- } return ms, err } // ReadString implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadString(s *smithy.Schema, v *string) error { if d.inHeaderList { *v = d.headerListValues[d.headerListIdx] d.headerListIdx++ return nil } if d.inBindings { if _, ok := isHTTPHeader(s); ok { hv, err := d.readHeaderString(s) if err != nil { return err } *v = hv return nil } if _, ok := smithy.SchemaTrait[*traits.HTTPPayload](s); ok { *v = string(d.payload) return nil } } if d.inPrefixMap { key := d.prefixKeys[d.prefixKeyIdx-1] *v = d.response.Header.Get(d.prefixValue + key) return nil } return d.body.ReadString(s, v) } // ReadBool implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadBool(s *smithy.Schema, v *bool) error { if !d.inBindings { return d.body.ReadBool(s, v) } name, _ := isHTTPHeader(s) var hv string if d.inHeaderList { hv = d.nextHeaderValue() } else { hv = d.response.Header.Get(name) } n, err := strconv.ParseBool(hv) if err != nil { return err } *v = n return nil } // ReadInt8 implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadInt8(s *smithy.Schema, v *int8) error { if !d.inBindings { return d.body.ReadInt8(s, v) } return readHeaderInt[int8](d, s, v) } // ReadInt16 implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadInt16(s *smithy.Schema, v *int16) error { if !d.inBindings { return d.body.ReadInt16(s, v) } return readHeaderInt[int16](d, s, v) } // ReadInt32 implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadInt32(s *smithy.Schema, v *int32) error { if !d.inBindings { return d.body.ReadInt32(s, v) } // https://smithy.io/2.0/spec/http-bindings.html#httpresponsecode-trait // // The httpResponseCode trait can be applied to structure members that // target an integer within any structure that has no input trait applied. if _, ok := smithy.SchemaTrait[*traits.HTTPResponseCode](s); ok { *v = int32(d.response.StatusCode) return nil } return readHeaderInt[int32](d, s, v) } // ReadInt64 implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadInt64(s *smithy.Schema, v *int64) error { if !d.inBindings { return d.body.ReadInt64(s, v) } return readHeaderInt[int64](d, s, v) } // ReadFloat32 implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadFloat32(s *smithy.Schema, v *float32) error { if !d.inBindings { return d.body.ReadFloat32(s, v) } return readHeaderFloat[float32](d, s, v) } // ReadFloat64 implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadFloat64(s *smithy.Schema, v *float64) error { if !d.inBindings { return d.body.ReadFloat64(s, v) } return readHeaderFloat[float64](d, s, v) } // ReadTime implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadTime(s *smithy.Schema, v *time.Time) error { if d.inHeaderList { return d.readHeaderListTime(func(t time.Time) { *v = t }) } if d.inBindings { t, err := d.readHeaderTime(s) if err != nil { return err } *v = t return nil } return d.body.ReadTime(s, v) } func (d *ShapeDeserializer) readHeaderTime(s *smithy.Schema) (time.Time, error) { name, _ := isHTTPHeader(s) hv := d.response.Header.Get(name) t, err := parseTimestamp(s, "http-date", hv) if err != nil { return time.Time{}, err } return t, nil } func (d *ShapeDeserializer) readHeaderListTime(assign func(time.Time)) error { hv := d.headerListValues[d.headerListIdx] d.headerListIdx++ t, err := parseTimestamp(nil, "http-date", hv) if err != nil { return err } assign(t) return nil } // ReadBlob implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadBlob(s *smithy.Schema, v *[]byte) error { if !d.inBindings { return d.body.ReadBlob(s, v) } if _, ok := smithy.SchemaTrait[*traits.HTTPPayload](s); ok { *v = d.payload return nil } name, ok := isHTTPHeader(s) if !ok { return fmt.Errorf("ReadBlob: unhandled http binding") } hv := d.response.Header.Get(name) b, err := base64.StdEncoding.DecodeString(hv) if err != nil { return err } *v = b return nil } // ReadList implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadList(s *smithy.Schema) error { if !d.inBindings { return d.body.ReadList(s) } name, ok := isHTTPHeader(s) if !ok { return fmt.Errorf("ReadList called outside of payload / http binding") } d.inHeaderList = true d.headerListIdx = 0 if s.ListMember() != nil && s.ListMember().Type() == smithy.ShapeTypeTimestamp && timestampFormat(s.ListMember(), "http-date") == "http-date" { vs, err := smithyhttp.SplitHTTPDateTimestampHeaderListValues(d.response.Header.Values(name)) if err != nil { return err } d.headerListValues = vs } else { vs, err := smithyhttp.SplitHeaderListValues(d.response.Header.Values(name)) if err != nil { return err } d.headerListValues = vs } return nil } // ReadListItem implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadListItem(s *smithy.Schema) (bool, error) { if !d.inHeaderList { return d.body.ReadListItem(s) } if d.headerListIdx >= len(d.headerListValues) { d.inHeaderList = false return false, nil } return true, nil } // ReadMap implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadMap(s *smithy.Schema) error { if !d.inBindings { return d.body.ReadMap(s) } ph, ok := smithy.SchemaTrait[*traits.HTTPPrefixHeaders](s) if !ok { return fmt.Errorf("ReadMap called outside of payload / http binding") } d.inPrefixMap = true d.prefixValue = ph.Prefix d.prefixKeyIdx = 0 canon := http.CanonicalHeaderKey(ph.Prefix) for name := range d.response.Header { if len(name) > len(canon) && strings.EqualFold(name[:len(canon)], canon) { d.prefixKeys = append(d.prefixKeys, strings.ToLower(name[len(canon):])) } } return nil } // ReadMapKey implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadMapKey(s *smithy.Schema) (string, bool, error) { if !d.inPrefixMap { return d.body.ReadMapKey(s) } if d.prefixKeyIdx >= len(d.prefixKeys) { d.inPrefixMap = false return "", false, nil } key := d.prefixKeys[d.prefixKeyIdx] d.prefixKeyIdx++ return key, true, nil } // ReadNil implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadNil(s *smithy.Schema) (bool, error) { return d.body.ReadNil(s) } // ReadDocument implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadDocument(s *smithy.Schema, v *document.Value) error { return d.body.ReadDocument(s, v) } // ReadUnion implements [smithy.ShapeDeserializer]. func (d *ShapeDeserializer) ReadUnion(s *smithy.Schema) (*smithy.Schema, error) { return d.body.ReadUnion(s) } func (d *ShapeDeserializer) isBindingSet(schema *smithy.Schema) bool { if name, ok := isHTTPHeader(schema); ok { return len(d.response.Header.Values(name)) > 0 } if trait, ok := isHTTPPrefixHeaders(schema); ok { canon := http.CanonicalHeaderKey(trait.Prefix) for name := range d.response.Header { if len(name) > len(canon) && strings.EqualFold(name[:len(canon)], canon) { return true } } return false } if _, ok := smithy.SchemaTrait[*traits.HTTPResponseCode](schema); ok { return true } if _, ok := smithy.SchemaTrait[*traits.HTTPPayload](schema); ok { return len(d.payload) > 0 } return false } func (d *ShapeDeserializer) readHeaderString(s *smithy.Schema) (string, error) { name, _ := isHTTPHeader(s) hv := d.response.Header.Get(name) if _, ok := smithy.SchemaTrait[*traits.MediaType](s); ok { b, err := base64.StdEncoding.DecodeString(hv) if err != nil { return "", err } hv = string(b) } return hv, nil } func (d *ShapeDeserializer) nextHeaderValue() string { v := d.headerListValues[d.headerListIdx] d.headerListIdx++ return v } type intn interface { int8 | int16 | int32 | int64 } func readHeaderInt[T intn](d *ShapeDeserializer, s *smithy.Schema, v *T) error { name, _ := isHTTPHeader(s) var hv string if d.inHeaderList { hv = d.nextHeaderValue() } else { hv = d.response.Header.Get(name) } n, err := strconv.ParseInt(hv, 10, 64) if err != nil { return err } *v = T(n) return nil } type floatn interface { float32 | float64 } func readHeaderFloat[T floatn](d *ShapeDeserializer, s *smithy.Schema, v *T) error { name, _ := isHTTPHeader(s) var hv string if d.inHeaderList { hv = d.nextHeaderValue() } else { hv = d.response.Header.Get(name) } n, err := strconv.ParseFloat(hv, 64) if err != nil { return err } *v = T(n) return nil } // ReadBigInt is unimplemented and will return an error. func (d *ShapeDeserializer) ReadBigInt(_ *smithy.Schema, _ *big.Int) error { return fmt.Errorf("unimplemented") } // ReadBigFloat is unimplemented and will return an error. func (d *ShapeDeserializer) ReadBigFloat(_ *smithy.Schema, _ *big.Float) error { return fmt.Errorf("unimplemented") }