Skip to content

Commit

Permalink
API: Improve MarshalToJson() in common/reflect/marshal.go (XTLS#3655)
Browse files Browse the repository at this point in the history
* Serialize enum to string in MarshalToJson().

* MarshalToJson() respect json tags.

* Add insertTypeInfo parameter to MarshalToJson().

* Omit empty string in MarshalToJson().

* Serialize PortList to string in MarshalToJson().

---------

Co-authored-by: nobody <nobody@nowhere.mars>
  • Loading branch information
2 people authored and leninalive committed Oct 29, 2024
1 parent ff07bc0 commit d5f1851
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 41 deletions.
135 changes: 107 additions & 28 deletions common/reflect/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,101 @@ package reflect

import (
"encoding/json"
"fmt"
"reflect"
"strings"

cnet "github.com/amnezia-vpn/amnezia-xray-core/common/net"
cserial "github.com/amnezia-vpn/amnezia-xray-core/common/serial"
"github.com/amnezia-vpn/amnezia-xray-core/infra/conf"
)

func MarshalToJson(v interface{}) (string, bool) {
if itf := marshalInterface(v, true); itf != nil {
func MarshalToJson(v interface{}, insertTypeInfo bool) (string, bool) {
if itf := marshalInterface(v, true, insertTypeInfo); itf != nil {
if b, err := json.MarshalIndent(itf, "", " "); err == nil {
return string(b[:]), true
}
}
return "", false
}

func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool) interface{} {
func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool, insertTypeInfo bool) interface{} {
if v == nil {
return nil
}
tmsg, err := v.GetInstance()
if err != nil {
return nil
}
r := marshalInterface(tmsg, ignoreNullValue)
if msg, ok := r.(map[string]interface{}); ok {
r := marshalInterface(tmsg, ignoreNullValue, insertTypeInfo)
if msg, ok := r.(map[string]interface{}); ok && insertTypeInfo {
msg["_TypedMessage_"] = v.Type
}
return r
}

func marshalSlice(v reflect.Value, ignoreNullValue bool) interface{} {
func marshalSlice(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} {
r := make([]interface{}, 0)
for i := 0; i < v.Len(); i++ {
rv := v.Index(i)
if rv.CanInterface() {
value := rv.Interface()
r = append(r, marshalInterface(value, ignoreNullValue))
r = append(r, marshalInterface(value, ignoreNullValue, insertTypeInfo))
}
}
return r
}

func marshalStruct(v reflect.Value, ignoreNullValue bool) interface{} {
func isNullValue(f reflect.StructField, rv reflect.Value) bool {
if rv.Kind() == reflect.String && rv.Len() == 0 {
return true
} else if !isValueKind(rv.Kind()) && rv.IsNil() {
return true
} else if tag := f.Tag.Get("json"); strings.Contains(tag, "omitempty") {
if !rv.IsValid() || rv.IsZero() {
return true
}
}
return false
}

func toJsonName(f reflect.StructField) string {
if tags := f.Tag.Get("protobuf"); len(tags) > 0 {
for _, tag := range strings.Split(tags, ",") {
if before, after, ok := strings.Cut(tag, "="); ok && before == "json" {
return after
}
}
}
if tag := f.Tag.Get("json"); len(tag) > 0 {
if before, _, ok := strings.Cut(tag, ","); ok {
return before
} else {
return tag
}
}
return f.Name
}

func marshalStruct(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} {
r := make(map[string]interface{})
t := v.Type()
for i := 0; i < v.NumField(); i++ {
rv := v.Field(i)
if rv.CanInterface() {
ft := t.Field(i)
name := ft.Name
value := rv.Interface()
tv := marshalInterface(value, ignoreNullValue)
if tv != nil || !ignoreNullValue {
if !ignoreNullValue || !isNullValue(ft, rv) {
name := toJsonName(ft)
value := rv.Interface()
tv := marshalInterface(value, ignoreNullValue, insertTypeInfo)
r[name] = tv
}
}
}
return r
}

func marshalMap(v reflect.Value, ignoreNullValue bool) interface{} {
func marshalMap(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} {
// policy.level is map[uint32] *struct
kt := v.Type().Key()
vt := reflect.TypeOf((*interface{})(nil))
Expand All @@ -71,7 +106,7 @@ func marshalMap(v reflect.Value, ignoreNullValue bool) interface{} {
rv := v.MapIndex(key)
if rv.CanInterface() {
iv := rv.Interface()
tv := marshalInterface(iv, ignoreNullValue)
tv := marshalInterface(iv, ignoreNullValue, insertTypeInfo)
if tv != nil || !ignoreNullValue {
r.SetMapIndex(key, reflect.ValueOf(&tv))
}
Expand All @@ -87,27 +122,63 @@ func marshalIString(v interface{}) (r string, ok bool) {
ok = false
}
}()

if iStringFn, ok := v.(interface{ String() string }); ok {
return iStringFn.String(), true
}
return "", false
}

func marshalKnownType(v interface{}, ignoreNullValue bool) (interface{}, bool) {
func serializePortList(portList *cnet.PortList) (interface{}, bool) {
if portList == nil {
return nil, false
}

n := len(portList.Range)
if n == 1 {
if first := portList.Range[0]; first.From == first.To {
return first.From, true
}
}

r := make([]string, 0, n)
for _, pr := range portList.Range {
if pr.From == pr.To {
r = append(r, pr.FromPort().String())
} else {
r = append(r, fmt.Sprintf("%d-%d", pr.From, pr.To))
}
}
return strings.Join(r, ","), true
}

func marshalKnownType(v interface{}, ignoreNullValue bool, insertTypeInfo bool) (interface{}, bool) {
switch ty := v.(type) {
case cserial.TypedMessage:
return marshalTypedMessage(&ty, ignoreNullValue), true
return marshalTypedMessage(&ty, ignoreNullValue, insertTypeInfo), true
case *cserial.TypedMessage:
return marshalTypedMessage(ty, ignoreNullValue), true
return marshalTypedMessage(ty, ignoreNullValue, insertTypeInfo), true
case map[string]json.RawMessage:
return ty, true
case []json.RawMessage:
return ty, true
case *json.RawMessage:
return ty, true
case json.RawMessage:
case *json.RawMessage, json.RawMessage:
return ty, true
case *cnet.IPOrDomain:
if domain := v.(*cnet.IPOrDomain); domain != nil {
return domain.AsAddress().String(), true
}
return nil, false
case *cnet.PortList:
npl := v.(*cnet.PortList)
return serializePortList(npl)
case *conf.PortList:
cpl := v.(*conf.PortList)
return serializePortList(cpl.Build())
case cnet.Address:
if addr := v.(cnet.Address); addr != nil {
return addr.String(), true
}
return nil, false
default:
return nil, false
}
Expand Down Expand Up @@ -138,9 +209,9 @@ func isValueKind(kind reflect.Kind) bool {
}
}

func marshalInterface(v interface{}, ignoreNullValue bool) interface{} {
func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} {

if r, ok := marshalKnownType(v, ignoreNullValue); ok {
if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok {
return r
}

Expand All @@ -152,19 +223,27 @@ func marshalInterface(v interface{}, ignoreNullValue bool) interface{} {
if k == reflect.Invalid {
return nil
}
if isValueKind(k) {

if ty := rv.Type().Name(); isValueKind(k) {
if k.String() != ty {
if s, ok := marshalIString(v); ok {
return s
}
}
return v
}

// fmt.Println("kind:", k, "type:", rv.Type().Name())

switch k {
case reflect.Struct:
return marshalStruct(rv, ignoreNullValue)
return marshalStruct(rv, ignoreNullValue, insertTypeInfo)
case reflect.Slice:
return marshalSlice(rv, ignoreNullValue)
return marshalSlice(rv, ignoreNullValue, insertTypeInfo)
case reflect.Array:
return marshalSlice(rv, ignoreNullValue)
return marshalSlice(rv, ignoreNullValue, insertTypeInfo)
case reflect.Map:
return marshalMap(rv, ignoreNullValue)
return marshalMap(rv, ignoreNullValue, insertTypeInfo)
default:
break
}
Expand Down
52 changes: 40 additions & 12 deletions common/reflect/marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,40 @@ import (
"strings"
"testing"

"github.com/amnezia-vpn/amnezia-xray-core/common/protocol"
. "github.com/amnezia-vpn/amnezia-xray-core/common/reflect"
cserial "github.com/amnezia-vpn/amnezia-xray-core/common/serial"
iserial "github.com/amnezia-vpn/amnezia-xray-core/infra/conf/serial"
"github.com/amnezia-vpn/amnezia-xray-core/proxy/shadowsocks"
)

func TestMashalAccount(t *testing.T) {
account := &shadowsocks.Account{
Password: "shadowsocks-password",
CipherType: shadowsocks.CipherType_CHACHA20_POLY1305,
}

user := &protocol.User{
Level: 0,
Email: "love@v2ray.com",
Account: cserial.ToTypedMessage(account),
}

j, ok := MarshalToJson(user, false)
if !ok || strings.Contains(j, "_TypedMessage_") {

t.Error("marshal account failed")
}

kws := []string{"CHACHA20_POLY1305", "cipherType", "shadowsocks-password"}
for _, kw := range kws {
if !strings.Contains(j, kw) {
t.Error("marshal account failed")
}
}
// t.Log(j)
}

func TestMashalStruct(t *testing.T) {
type Foo = struct {
N int `json:"n"`
Expand All @@ -36,8 +65,8 @@ func TestMashalStruct(t *testing.T) {
Arr: &arr,
}

s, ok1 := MarshalToJson(f1)
sp, ok2 := MarshalToJson(&f1)
s, ok1 := MarshalToJson(f1, true)
sp, ok2 := MarshalToJson(&f1, true)

if !ok1 || !ok2 || s != sp {
t.Error("marshal failed")
Expand Down Expand Up @@ -69,7 +98,7 @@ func TestMarshalConfigJson(t *testing.T) {
}

tmsg := cserial.ToTypedMessage(bc)
tc, ok := MarshalToJson(tmsg)
tc, ok := MarshalToJson(tmsg, true)
if !ok {
t.Error("marshal config failed")
}
Expand All @@ -79,15 +108,14 @@ func TestMarshalConfigJson(t *testing.T) {
keywords := []string{
"4784f9b8-a879-4fec-9718-ebddefa47750",
"bing.com",
"DomainStrategy",
"InboundTag",
"Level",
"Stats",
"UserDownlink",
"UserUplink",
"System",
"InboundDownlink",
"OutboundUplink",
"inboundTag",
"level",
"stats",
"userDownlink",
"userUplink",
"system",
"inboundDownlink",
"outboundUplink",
}
for _, kw := range keywords {
if !strings.Contains(tc, kw) {
Expand Down
2 changes: 1 addition & 1 deletion infra/conf/serial/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func MergeConfigFromFiles(files []string, formats []string) (string, error) {
return "", err
}

if j, ok := creflect.MarshalToJson(c); ok {
if j, ok := creflect.MarshalToJson(c, true); ok {
return j, nil
}
return "", errors.New("marshal to json failed.").AtError()
Expand Down

0 comments on commit d5f1851

Please sign in to comment.