From ef5cfd59d4425ea59493ac93aff9a30a5afa1429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 3 Jul 2022 11:28:15 +0800 Subject: [PATCH] Ordered json output & Disallow unknown fields --- adapter/route/rule_domain.go | 2 +- cmd/sing-box/main.go | 4 +- {adapter/route => common}/domain/matcher.go | 0 .../route => common}/domain/matcher_test.go | 0 {adapter/route => common}/domain/set.go | 0 common/linkedhashmap/map.go | 171 ++++++++++++++++++ go.mod | 3 +- go.sum | 6 +- option/address.go | 3 +- option/config.go | 17 +- option/inbound.go | 9 +- option/json.go | 31 +++- option/listable.go | 2 +- option/network.go | 3 +- option/outbound.go | 9 +- option/route.go | 9 +- 16 files changed, 243 insertions(+), 26 deletions(-) rename {adapter/route => common}/domain/matcher.go (100%) rename {adapter/route => common}/domain/matcher_test.go (100%) rename {adapter/route => common}/domain/set.go (100%) create mode 100644 common/linkedhashmap/map.go diff --git a/adapter/route/rule_domain.go b/adapter/route/rule_domain.go index 518652f9..ffb1b114 100644 --- a/adapter/route/rule_domain.go +++ b/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" ) diff --git a/cmd/sing-box/main.go b/cmd/sing-box/main.go index 082022db..af29dd03 100644 --- a/cmd/sing-box/main.go +++ b/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()) diff --git a/adapter/route/domain/matcher.go b/common/domain/matcher.go similarity index 100% rename from adapter/route/domain/matcher.go rename to common/domain/matcher.go diff --git a/adapter/route/domain/matcher_test.go b/common/domain/matcher_test.go similarity index 100% rename from adapter/route/domain/matcher_test.go rename to common/domain/matcher_test.go diff --git a/adapter/route/domain/set.go b/common/domain/set.go similarity index 100% rename from adapter/route/domain/set.go rename to common/domain/set.go diff --git a/common/linkedhashmap/map.go b/common/linkedhashmap/map.go new file mode 100644 index 00000000..cdae5d9f --- /dev/null +++ b/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]{} +} diff --git a/go.mod b/go.mod index 6b49831f..4798f188 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 1beb0a55..a7c131dc 100644 --- a/go.sum +++ b/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= diff --git a/option/address.go b/option/address.go index db179e2b..cf11ff71 100644 --- a/option/address.go +++ b/option/address.go @@ -1,8 +1,9 @@ package option import ( - "encoding/json" "net/netip" + + "github.com/goccy/go-json" ) type ListenAddress netip.Addr diff --git a/option/config.go b/option/config.go index 2b091714..4946f742 100644 --- a/option/config.go +++ b/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) && diff --git a/option/inbound.go b/option/inbound.go index f18948c3..bf0e1785 100644 --- a/option/inbound.go +++ b/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 { diff --git a/option/json.go b/option/json.go index c1f08dfc..bf4170cb 100644 --- a/option/json.go +++ b/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) } diff --git a/option/listable.go b/option/listable.go index d74a9511..e93d6c63 100644 --- a/option/listable.go +++ b/option/listable.go @@ -1,6 +1,6 @@ package option -import "encoding/json" +import "github.com/goccy/go-json" type Listable[T comparable] []T diff --git a/option/network.go b/option/network.go index 82f0b557..6e66a672 100644 --- a/option/network.go +++ b/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" ) diff --git a/option/outbound.go b/option/outbound.go index 09d310c6..78a3ff4e 100644 --- a/option/outbound.go +++ b/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 { diff --git a/option/route.go b/option/route.go index cb2d6c57..ab4164b9 100644 --- a/option/route.go +++ b/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 {