Browse Source

API: Improve MarshalToJson() in common/reflect/marshal.go (#3655)

* 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 <[email protected]>
nobody 1 year ago
parent
commit
ac628a9427
3 changed files with 148 additions and 41 deletions
  1. 107 28
      common/reflect/marshal.go
  2. 40 12
      common/reflect/marshal_test.go
  3. 1 1
      infra/conf/serial/builder.go

+ 107 - 28
common/reflect/marshal.go

@@ -2,13 +2,17 @@ package reflect
 
 import (
 	"encoding/json"
+	"fmt"
 	"reflect"
+	"strings"
 
+	cnet "github.com/xtls/xray-core/common/net"
 	cserial "github.com/xtls/xray-core/common/serial"
+	"github.com/xtls/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
 		}
@@ -16,7 +20,7 @@ func MarshalToJson(v interface{}) (string, bool) {
 	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
 	}
@@ -24,36 +28,67 @@ func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool) interfac
 	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
 			}
 		}
@@ -61,7 +96,7 @@ func marshalStruct(v reflect.Value, ignoreNullValue bool) interface{} {
 	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))
@@ -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))
 			}
@@ -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
 	}
@@ -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
 	}
 
@@ -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
 	}

+ 40 - 12
common/reflect/marshal_test.go

@@ -6,11 +6,40 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/xtls/xray-core/common/protocol"
 	. "github.com/xtls/xray-core/common/reflect"
 	cserial "github.com/xtls/xray-core/common/serial"
 	iserial "github.com/xtls/xray-core/infra/conf/serial"
+	"github.com/xtls/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:   "[email protected]",
+		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"`
@@ -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")
@@ -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")
 	}
@@ -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) {

+ 1 - 1
infra/conf/serial/builder.go

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