From adab3c264e13ab028e88aba561030388b1277e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Mon, 16 Sep 2024 23:01:08 +0100 Subject: [PATCH] api: massage protojson's output to use hex rather than base64 Fixes #1354. --- api/helpers.go | 49 +++++++++++++++++++++++++++++++++++++++++++++ api/helpers_test.go | 4 ++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/api/helpers.go b/api/helpers.go index a7431c296..c47bcbc63 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -1,12 +1,15 @@ package api import ( + "bytes" + "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "math" "math/big" + "reflect" "strconv" "strings" "time" @@ -71,9 +74,55 @@ func protoTxAsJSON(tx []byte) []byte { if err != nil { panic(err) } + // protojson follows protobuf's json mapping behavior, + // which requires bytes to be encoded as base64: + // https://protobuf.dev/programming-guides/proto3/#json + // + // We want hex rather than base64 for consistency with our REST API + // protojson does not expose any option to configure its behavior, + // and as the Go types are code generated, we cannot use our HexBytes type. + // + // Do a bit of a hack: walk the protobuf value with reflection, + // find all []byte values, and search-and-replace their base64 encoding with hex + // in the protojson bytes. This can be slow if we have many byte slices, + // but in general the protobuf message will be small so there will be few. + bytesValues := collectBytesValues(nil, reflect.ValueOf(&ptx)) + for _, bv := range bytesValues { + asBase64 := base64.StdEncoding.AppendEncode(nil, bv) + asHex := hex.AppendEncode(nil, bv) + println(string(asBase64), string(asHex)) + asJSON = bytes.Replace(asJSON, asBase64, asHex, 1) + } return asJSON } +var typBytes = reflect.TypeFor[[]byte]() + +func collectBytesValues(result [][]byte, val reflect.Value) [][]byte { + typ := val.Type() + if typ == typBytes { + return append(result, val.Bytes()) + } + switch typ.Kind() { + case reflect.Pointer, reflect.Interface: + if !val.IsNil() { + result = collectBytesValues(result, val.Elem()) + } + case reflect.Struct: + for i := 0; i < val.NumField(); i++ { + if !typ.Field(i).IsExported() { + continue + } + result = collectBytesValues(result, val.Field(i)) + } + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + result = collectBytesValues(result, val.Index(i)) + } + } + return result +} + // isTransactionType checks if the given transaction is of the given type. // t is expected to be a pointer to a protobuf transaction message. func isTransactionType[T any](signedTxBytes []byte) (bool, error) { diff --git a/api/helpers_test.go b/api/helpers_test.go index 489a64e9b..61913b028 100644 --- a/api/helpers_test.go +++ b/api/helpers_test.go @@ -161,8 +161,8 @@ func TestProtoTxAsJSON(t *testing.T) { "setProcess": { "txtype": "SET_PROCESS_CENSUS", "nonce": 1, - "processId": "sx3/YYFNq5DWw6m2XWyQgwSA5Lda0y50eUICAAAAAAA=", - "censusRoot": "zUU9BcTLBCnuXuGu/tAW9VO4AmtM7VsMNSkFv6U8foE=", + "processId": "b31dff61814dab90d6c3a9b65d6c90830480e4b75ad32e747942020000000000", + "censusRoot": "cd453d05c4cb0429ee5ee1aefed016f553b8026b4ced5b0c352905bfa53c7e81", "censusURI": "ipfs://bafybeicyfukarcryrvy5oe37ligmxwf55sbfiojori4t25wencma4ymxfa", "censusSize": "1000" }