Browse Source

Ordered json output & Disallow unknown fields

世界 3 years ago
parent
commit
ef5cfd59d4

+ 1 - 1
adapter/route/rule_domain.go

@@ -4,7 +4,7 @@ import (
 	"strings"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/adapter/route/domain"
+	"github.com/sagernet/sing-box/common/domain"
 	"github.com/sagernet/sing/common"
 )
 

+ 2 - 2
cmd/sing-box/main.go

@@ -2,11 +2,11 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"os"
 	"os/signal"
 	"syscall"
 
+	"github.com/goccy/go-json"
 	"github.com/sagernet/sing-box"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sirupsen/logrus"
@@ -51,7 +51,7 @@ func run(cmd *cobra.Command, args []string) {
 	var options option.Options
 	err = json.Unmarshal(configContent, &options)
 	if err != nil {
-		logrus.Fatal("parse config: ", err)
+		logrus.Fatal("decode config: ", err)
 	}
 
 	ctx, cancel := context.WithCancel(context.Background())

+ 0 - 0
adapter/route/domain/matcher.go → common/domain/matcher.go


+ 0 - 0
adapter/route/domain/matcher_test.go → common/domain/matcher_test.go


+ 0 - 0
adapter/route/domain/set.go → common/domain/set.go


+ 171 - 0
common/linkedhashmap/map.go

@@ -0,0 +1,171 @@
+package linkedhashmap
+
+import (
+	"bytes"
+
+	"github.com/goccy/go-json"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/x/list"
+)
+
+type Map[K comparable, V any] struct {
+	raw    list.List[mapEntry[K, V]]
+	rawMap map[K]*list.Element[mapEntry[K, V]]
+}
+
+func (m *Map[K, V]) init() {
+	if m.rawMap == nil {
+		m.rawMap = make(map[K]*list.Element[mapEntry[K, V]])
+	}
+}
+
+func (m *Map[K, V]) MarshalJSON() ([]byte, error) {
+	buffer := new(bytes.Buffer)
+	buffer.WriteString("{")
+	for item := m.raw.Front(); item != nil; {
+		entry := item.Value
+		err := json.NewEncoder(buffer).Encode(entry.Key)
+		if err != nil {
+			return nil, err
+		}
+		buffer.WriteString(": ")
+		err = json.NewEncoder(buffer).Encode(entry.Value)
+		if err != nil {
+			return nil, err
+		}
+		item = item.Next()
+		if item != nil {
+			buffer.WriteString(", ")
+		}
+	}
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (m *Map[K, V]) UnmarshalJSON(content []byte) error {
+	decoder := json.NewDecoder(bytes.NewReader(content))
+	m.Clear()
+	m.init()
+	objectStart, err := decoder.Token()
+	if err != nil {
+		return err
+	} else if objectStart != json.Delim('{') {
+		return E.New("expected json object start, but starts with ", objectStart)
+	}
+	for decoder.More() {
+		var entryKey K
+		err = decoder.Decode(&entryKey)
+		if err != nil {
+			return err
+		}
+		var entryValue V
+		err = decoder.Decode(&entryValue)
+		if err != nil {
+			return err
+		}
+		m.rawMap[entryKey] = m.raw.PushBack(mapEntry[K, V]{Key: entryKey, Value: entryValue})
+	}
+	objectEnd, err := decoder.Token()
+	if err != nil {
+		return err
+	} else if objectEnd != json.Delim('}') {
+		return E.New("expected json object end, but ends with ", objectEnd)
+	}
+	return nil
+}
+
+type mapEntry[K comparable, V any] struct {
+	Key   K
+	Value V
+}
+
+func (m *Map[K, V]) Size() int {
+	return m.raw.Size()
+}
+
+func (m *Map[K, V]) IsEmpty() bool {
+	return m.raw.IsEmpty()
+}
+
+func (m *Map[K, V]) ContainsKey(key K) bool {
+	m.init()
+	_, loaded := m.rawMap[key]
+	return loaded
+}
+
+func (m *Map[K, V]) Get(key K) (V, bool) {
+	m.init()
+	value, loaded := m.rawMap[key]
+	return value.Value.Value, loaded
+}
+
+func (m *Map[K, V]) Put(key K, value V) V {
+	m.init()
+	entry, loaded := m.rawMap[key]
+	if loaded {
+		oldValue := entry.Value.Value
+		entry.Value.Value = value
+		return oldValue
+	}
+	entry = m.raw.PushBack(mapEntry[K, V]{Key: key, Value: value})
+	m.rawMap[key] = entry
+	return common.DefaultValue[V]()
+}
+
+func (m *Map[K, V]) PutAll(other *Map[K, V]) {
+	for item := other.raw.Front(); item != nil; item = item.Next() {
+		m.Put(item.Value.Key, item.Value.Value)
+	}
+}
+
+func (m *Map[K, V]) Remove(key K) bool {
+	m.init()
+	entry, loaded := m.rawMap[key]
+	if !loaded {
+		return false
+	}
+	m.raw.Remove(entry)
+	delete(m.rawMap, key)
+	return true
+}
+
+func (m *Map[K, V]) RemoveAll(keys []K) {
+	m.init()
+	for _, key := range keys {
+		entry, loaded := m.rawMap[key]
+		if !loaded {
+			continue
+		}
+		m.raw.Remove(entry)
+		delete(m.rawMap, key)
+	}
+}
+
+func (m *Map[K, V]) AsMap() map[K]V {
+	result := make(map[K]V, m.raw.Len())
+	for item := m.raw.Front(); item != nil; item = item.Next() {
+		result[item.Value.Key] = item.Value.Value
+	}
+	return result
+}
+
+func (m *Map[K, V]) Keys() []K {
+	result := make([]K, 0, m.raw.Len())
+	for item := m.raw.Front(); item != nil; item = item.Next() {
+		result = append(result, item.Value.Key)
+	}
+	return result
+}
+
+func (m *Map[K, V]) Values() []V {
+	result := make([]V, 0, m.raw.Len())
+	for item := m.raw.Front(); item != nil; item = item.Next() {
+		result = append(result, item.Value.Value)
+	}
+	return result
+}
+
+func (m *Map[K, V]) Clear() {
+	*m = Map[K, V]{}
+}

+ 2 - 1
go.mod

@@ -4,9 +4,10 @@ go 1.18
 
 require (
 	github.com/database64128/tfo-go v1.0.4
+	github.com/goccy/go-json v0.9.8
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/oschwald/geoip2-golang v1.7.0
-	github.com/sagernet/sing v0.0.0-20220702193452-6a6c180cf77e
+	github.com/sagernet/sing v0.0.0-20220703025722-d002d5ba3ba5
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.5.0

+ 4 - 2
go.sum

@@ -4,6 +4,8 @@ github.com/database64128/tfo-go v1.0.4/go.mod h1:q5W+W0+2IHrw/Lnl0yg4sz7Kz5IDsm9
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
+github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -18,8 +20,8 @@ github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagernet/sing v0.0.0-20220702193452-6a6c180cf77e h1:GiH/gZcH8fupEfWJujaqGNBBaCkAouAJyVJy9Afxfvw=
-github.com/sagernet/sing v0.0.0-20220702193452-6a6c180cf77e/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing v0.0.0-20220703025722-d002d5ba3ba5 h1:0oNnpN43Z6TXDdea5tbKIXXJ62yJqTpOW4IyQSgN3KY=
+github.com/sagernet/sing v0.0.0-20220703025722-d002d5ba3ba5/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=

+ 2 - 1
option/address.go

@@ -1,8 +1,9 @@
 package option
 
 import (
-	"encoding/json"
 	"net/netip"
+
+	"github.com/goccy/go-json"
 )
 
 type ListenAddress netip.Addr

+ 15 - 2
option/config.go

@@ -1,14 +1,27 @@
 package option
 
-import "github.com/sagernet/sing/common"
+import (
+	"bytes"
 
-type Options struct {
+	"github.com/goccy/go-json"
+	"github.com/sagernet/sing/common"
+)
+
+type _Options struct {
 	Log       *LogOption    `json:"log,omitempty"`
 	Inbounds  []Inbound     `json:"inbounds,omitempty"`
 	Outbounds []Outbound    `json:"outbounds,omitempty"`
 	Route     *RouteOptions `json:"route,omitempty"`
 }
 
+type Options _Options
+
+func (o *Options) UnmarshalJSON(content []byte) error {
+	decoder := json.NewDecoder(bytes.NewReader(content))
+	decoder.DisallowUnknownFields()
+	return decoder.Decode((*_Options)(o))
+}
+
 func (o Options) Equals(other Options) bool {
 	return common.ComparablePtrEquals(o.Log, other.Log) &&
 		common.SliceEquals(o.Inbounds, other.Inbounds) &&

+ 6 - 3
option/inbound.go

@@ -1,8 +1,7 @@
 package option
 
 import (
-	"encoding/json"
-
+	"github.com/goccy/go-json"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -69,7 +68,11 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 	default:
 		return nil
 	}
-	return json.Unmarshal(bytes, v)
+	err = UnmarshallExcluded(bytes, (*_Inbound)(h), v)
+	if err != nil {
+		return E.Cause(err, "inbound options")
+	}
+	return nil
 }
 
 type ListenOptions struct {

+ 26 - 5
option/json.go

@@ -1,18 +1,19 @@
 package option
 
 import (
-	"encoding/json"
+	"bytes"
 
-	"github.com/sagernet/sing/common/x/linkedhashmap"
+	"github.com/goccy/go-json"
+	"github.com/sagernet/sing-box/common/linkedhashmap"
 )
 
 func ToMap(v any) (*linkedhashmap.Map[string, any], error) {
-	bytes, err := json.Marshal(v)
+	inputContent, err := json.Marshal(v)
 	if err != nil {
 		return nil, err
 	}
 	var content linkedhashmap.Map[string, any]
-	err = json.Unmarshal(bytes, &content)
+	err = content.UnmarshalJSON(inputContent)
 	if err != nil {
 		return nil, err
 	}
@@ -36,5 +37,25 @@ func MarshallObjects(objects ...any) ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	return json.Marshal(content)
+	return content.MarshalJSON()
+}
+
+func UnmarshallExcluded(inputContent []byte, parentObject any, object any) error {
+	parentContent, err := ToMap(parentObject)
+	if err != nil {
+		return err
+	}
+	var content linkedhashmap.Map[string, any]
+	err = content.UnmarshalJSON(inputContent)
+	if err != nil {
+		return err
+	}
+	content.RemoveAll(parentContent.Keys())
+	inputContent, err = content.MarshalJSON()
+	if err != nil {
+		return err
+	}
+	decoder := json.NewDecoder(bytes.NewReader(inputContent))
+	decoder.DisallowUnknownFields()
+	return decoder.Decode(object)
 }

+ 1 - 1
option/listable.go

@@ -1,6 +1,6 @@
 package option
 
-import "encoding/json"
+import "github.com/goccy/go-json"
 
 type Listable[T comparable] []T
 

+ 1 - 2
option/network.go

@@ -1,8 +1,7 @@
 package option
 
 import (
-	"encoding/json"
-
+	"github.com/goccy/go-json"
 	C "github.com/sagernet/sing-box/constant"
 	E "github.com/sagernet/sing/common/exceptions"
 )

+ 6 - 3
option/outbound.go

@@ -1,8 +1,7 @@
 package option
 
 import (
-	"encoding/json"
-
+	"github.com/goccy/go-json"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 )
@@ -43,7 +42,11 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
 	default:
 		return nil
 	}
-	return json.Unmarshal(bytes, v)
+	err = UnmarshallExcluded(bytes, (*_Outbound)(h), v)
+	if err != nil {
+		return E.Cause(err, "outbound options")
+	}
+	return nil
 }
 
 type DialerOptions struct {

+ 6 - 3
option/route.go

@@ -1,8 +1,7 @@
 package option
 
 import (
-	"encoding/json"
-
+	"github.com/goccy/go-json"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -68,7 +67,11 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error {
 	default:
 		return E.New("unknown rule type: " + r.Type)
 	}
-	return json.Unmarshal(bytes, v)
+	err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
+	if err != nil {
+		return E.Cause(err, "route rule")
+	}
+	return nil
 }
 
 type DefaultRule struct {