fix(GODT-2212): Preserve header order when building messages

Ensure order of parsed header field is recorded alongside the values.
This commit is contained in:
Leander Beernaert
2023-09-25 10:56:38 +02:00
committed by LBeernaertProton
parent 3de73982c7
commit 331ad8e6d5
6 changed files with 103 additions and 11 deletions

1
go.mod
View File

@@ -18,6 +18,7 @@ require (
github.com/sirupsen/logrus v1.9.2 github.com/sirupsen/logrus v1.9.2
github.com/stretchr/testify v1.8.3 github.com/stretchr/testify v1.8.3
github.com/urfave/cli/v2 v2.24.4 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 go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.10.0 golang.org/x/net v0.10.0

2
go.sum
View File

@@ -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 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 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= 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 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 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= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=

View File

@@ -3,33 +3,50 @@ package proton
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"gitlab.com/c0b/go-ordered-json"
) )
var ErrBadHeader = errors.New("bad header") 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 { func (h *Headers) UnmarshalJSON(b []byte) error {
type rawHeaders map[string]any type rawHeaders map[string]any
raw := make(rawHeaders) 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 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 { iter := orderedMap.EntriesIter()
switch val := val.(type) {
for {
entry, ok := iter()
if !ok {
break
}
switch val := entry.Value.(type) {
case string: case string:
header[key] = []string{val} header.Values[entry.Key] = []string{val}
case []any: case []any:
for _, val := range val { for _, val := range val {
switch val := val.(type) { switch val := val.(type) {
case string: case string:
header[key] = append(header[key], val) header.Values[entry.Key] = append(header.Values[entry.Key], val)
default: default:
return ErrBadHeader return ErrBadHeader
@@ -39,9 +56,54 @@ func (h *Headers) UnmarshalJSON(b []byte) error {
default: default:
return ErrBadHeader return ErrBadHeader
} }
header.Order = append(header.Order, entry.Key)
} }
*h = header *h = header
return nil 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
}

24
header_types_test.go Normal file
View File

@@ -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)
}

View File

@@ -295,8 +295,8 @@ func getTextPartHeader(body []byte, mimeType rfc822.MIMEType) message.Header {
func getAttachmentPartHeader(att Attachment) message.Header { func getAttachmentPartHeader(att Attachment) message.Header {
var header message.Header var header message.Header
for key, val := range att.Headers { for _, key := range att.Headers.Order {
for _, val := range val { for _, val := range att.Headers.Values[key] {
header.Add(key, val) header.Add(key, val)
} }
} }

View File

@@ -237,10 +237,13 @@ func (msg *message) getParsedHeaders() proton.Headers {
panic(err) panic(err)
} }
parsed := make(proton.Headers) parsed := proton.Headers{
Values: make(map[string][]string),
}
header.Entries(func(key, value 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 return parsed