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/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

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/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=

View File

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

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

View File

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