From 331ad8e6d5ee4123dc13d9ff2ff008b55a457e92 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Mon, 25 Sep 2023 10:56:38 +0200 Subject: [PATCH] fix(GODT-2212): Preserve header order when building messages Ensure order of parsed header field is recorded alongside the values. --- go.mod | 1 + go.sum | 2 ++ header_types.go | 76 +++++++++++++++++++++++++++++++++++---- header_types_test.go | 24 +++++++++++++ message_build.go | 4 +-- server/backend/message.go | 7 ++-- 6 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 header_types_test.go diff --git a/go.mod b/go.mod index 35d4738..245b181 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/sirupsen/logrus v1.9.2 github.com/stretchr/testify v1.8.3 github.com/urfave/cli/v2 v2.24.4 + gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a go.uber.org/goleak v1.2.1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/net v0.10.0 diff --git a/go.sum b/go.sum index 967eb82..07f3486 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,8 @@ github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6f github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k= +gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= diff --git a/header_types.go b/header_types.go index 8623035..051ce91 100644 --- a/header_types.go +++ b/header_types.go @@ -3,33 +3,50 @@ package proton import ( "encoding/json" "errors" + "gitlab.com/c0b/go-ordered-json" ) var ErrBadHeader = errors.New("bad header") -type Headers map[string][]string +type Headers struct { + Values map[string][]string + Order []string +} func (h *Headers) UnmarshalJSON(b []byte) error { type rawHeaders map[string]any raw := make(rawHeaders) - if err := json.Unmarshal(b, &raw); err != nil { + // Need to use a different type to deserialize, because there still is no official way for json to decode an object + // with the fields in order https://github.com/golang/go/issues/27179. + orderedMap := ordered.NewOrderedMap() + if err := orderedMap.UnmarshalJSON(b); err != nil { return err } - header := make(Headers) + header := Headers{ + Values: make(map[string][]string, len(raw)), + Order: make([]string, 0, len(raw)), + } - for key, val := range raw { - switch val := val.(type) { + iter := orderedMap.EntriesIter() + + for { + entry, ok := iter() + if !ok { + break + } + + switch val := entry.Value.(type) { case string: - header[key] = []string{val} + header.Values[entry.Key] = []string{val} case []any: for _, val := range val { switch val := val.(type) { case string: - header[key] = append(header[key], val) + header.Values[entry.Key] = append(header.Values[entry.Key], val) default: return ErrBadHeader @@ -39,9 +56,54 @@ func (h *Headers) UnmarshalJSON(b []byte) error { default: return ErrBadHeader } + + header.Order = append(header.Order, entry.Key) } *h = header return nil } + +func (h Headers) MarshalJSON() ([]byte, error) { + // Manually Serialize to preserve oder + if len(h.Values) == 0 { + return []byte{'{', '}'}, nil + } + + out := make([]byte, 0, 64) + + out = append(out, '{') + + for _, k := range h.Order { + v := h.Values[k] + + if len(v) == 0 { + continue + } + + key, err := json.Marshal(k) + if err != nil { + return nil, err + } + + var val []byte + if len(v) == 1 { + val, err = json.Marshal(v[0]) + } else { + val, err = json.Marshal(v) + } + if err != nil { + return nil, err + } + + out = append(out, key...) + out = append(out, ':') + out = append(out, val...) + out = append(out, ',') + } + + out[len(out)-1] = '}' + + return out, nil +} diff --git a/header_types_test.go b/header_types_test.go new file mode 100644 index 0000000..9ffa420 --- /dev/null +++ b/header_types_test.go @@ -0,0 +1,24 @@ +package proton + +import ( + "encoding/json" + "github.com/stretchr/testify/require" + "testing" +) + +func TestHeaders_MarshalInOrder(t *testing.T) { + jsonBytes := []byte(`{"zz":"v1","foo":["a","b"],"bar":"30"}`) + + var h Headers + + err := json.Unmarshal(jsonBytes, &h) + require.NoError(t, err) + + expectedKeyOrder := []string{"zz", "foo", "bar"} + + require.Equal(t, expectedKeyOrder, h.Order) + + serializedJson, err := json.Marshal(h) + require.NoError(t, err) + require.Equal(t, jsonBytes, serializedJson) +} diff --git a/message_build.go b/message_build.go index 1a2e96d..b4a1f29 100644 --- a/message_build.go +++ b/message_build.go @@ -295,8 +295,8 @@ func getTextPartHeader(body []byte, mimeType rfc822.MIMEType) message.Header { func getAttachmentPartHeader(att Attachment) message.Header { var header message.Header - for key, val := range att.Headers { - for _, val := range val { + for _, key := range att.Headers.Order { + for _, val := range att.Headers.Values[key] { header.Add(key, val) } } diff --git a/server/backend/message.go b/server/backend/message.go index 6f54482..8a0d20c 100644 --- a/server/backend/message.go +++ b/server/backend/message.go @@ -237,10 +237,13 @@ func (msg *message) getParsedHeaders() proton.Headers { panic(err) } - parsed := make(proton.Headers) + parsed := proton.Headers{ + Values: make(map[string][]string), + } header.Entries(func(key, value string) { - parsed[key] = append(parsed[key], value) + parsed.Order = append(parsed.Order, key) + parsed.Values[key] = append(parsed.Values[key], value) }) return parsed