From d962a01bf727a167af7b7c2fd70ba75f10cb2df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 5 Dec 2023 14:26:35 +0800 Subject: [PATCH] Migrate json wrapper and badjson to library --- common/json/badjson/array.go | 46 ++++++++++++ common/json/badjson/json.go | 54 ++++++++++++++ common/json/badjson/merge.go | 79 +++++++++++++++++++++ common/json/badjson/object.go | 79 +++++++++++++++++++++ common/json/badjson/typed.go | 79 +++++++++++++++++++++ common/json/comment.go | 128 ++++++++++++++++++++++++++++++++++ common/json/context.go | 23 ++++++ common/json/std.go | 21 ++++++ 8 files changed, 509 insertions(+) create mode 100644 common/json/badjson/array.go create mode 100644 common/json/badjson/json.go create mode 100644 common/json/badjson/merge.go create mode 100644 common/json/badjson/object.go create mode 100644 common/json/badjson/typed.go create mode 100644 common/json/comment.go create mode 100644 common/json/context.go create mode 100644 common/json/std.go diff --git a/common/json/badjson/array.go b/common/json/badjson/array.go new file mode 100644 index 0000000..f662f18 --- /dev/null +++ b/common/json/badjson/array.go @@ -0,0 +1,46 @@ +package badjson + +import ( + "bytes" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" +) + +type JSONArray []any + +func (a JSONArray) MarshalJSON() ([]byte, error) { + return json.Marshal([]any(a)) +} + +func (a *JSONArray) UnmarshalJSON(content []byte) error { + decoder := json.NewDecoder(bytes.NewReader(content)) + arrayStart, err := decoder.Token() + if err != nil { + return err + } else if arrayStart != json.Delim('[') { + return E.New("excepted array start, but got ", arrayStart) + } + err = a.decodeJSON(decoder) + if err != nil { + return err + } + arrayEnd, err := decoder.Token() + if err != nil { + return err + } else if arrayEnd != json.Delim(']') { + return E.New("excepted array end, but got ", arrayEnd) + } + return nil +} + +func (a *JSONArray) decodeJSON(decoder *json.Decoder) error { + for decoder.More() { + item, err := decodeJSON(decoder) + if err != nil { + return err + } + *a = append(*a, item) + } + return nil +} diff --git a/common/json/badjson/json.go b/common/json/badjson/json.go new file mode 100644 index 0000000..04dba1e --- /dev/null +++ b/common/json/badjson/json.go @@ -0,0 +1,54 @@ +package badjson + +import ( + "bytes" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" +) + +func Decode(content []byte) (any, error) { + decoder := json.NewDecoder(bytes.NewReader(content)) + return decodeJSON(decoder) +} + +func decodeJSON(decoder *json.Decoder) (any, error) { + rawToken, err := decoder.Token() + if err != nil { + return nil, err + } + switch token := rawToken.(type) { + case json.Delim: + switch token { + case '{': + var object JSONObject + err = object.decodeJSON(decoder) + if err != nil { + return nil, err + } + rawToken, err = decoder.Token() + if err != nil { + return nil, err + } else if rawToken != json.Delim('}') { + return nil, E.New("excepted object end, but got ", rawToken) + } + return &object, nil + case '[': + var array JSONArray + err = array.decodeJSON(decoder) + if err != nil { + return nil, err + } + rawToken, err = decoder.Token() + if err != nil { + return nil, err + } else if rawToken != json.Delim(']') { + return nil, E.New("excepted array end, but got ", rawToken) + } + return array, nil + default: + return nil, E.New("excepted object or array end: ", token) + } + } + return rawToken, nil +} diff --git a/common/json/badjson/merge.go b/common/json/badjson/merge.go new file mode 100644 index 0000000..a2fd49d --- /dev/null +++ b/common/json/badjson/merge.go @@ -0,0 +1,79 @@ +package badjson + +import ( + "reflect" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" +) + +func MergeOptions[T any](source T, destination T) (T, error) { + rawSource, err := json.Marshal(source) + if err != nil { + return common.DefaultValue[T](), E.Cause(err, "marshal source") + } + rawDestination, err := json.Marshal(destination) + if err != nil { + return common.DefaultValue[T](), E.Cause(err, "marshal destination") + } + rawMerged, err := MergeJSON(rawSource, rawDestination) + if err != nil { + return common.DefaultValue[T](), E.Cause(err, "merge options") + } + var merged T + err = json.Unmarshal(rawMerged, &merged) + if err != nil { + return common.DefaultValue[T](), E.Cause(err, "unmarshal merged options") + } + return merged, nil +} + +func MergeJSON(rawSource json.RawMessage, rawDestination json.RawMessage) (json.RawMessage, error) { + source, err := Decode(rawSource) + if err != nil { + return nil, E.Cause(err, "decode source") + } + destination, err := Decode(rawDestination) + if err != nil { + return nil, E.Cause(err, "decode destination") + } + merged, err := mergeJSON(source, destination) + if err != nil { + return nil, err + } + return json.Marshal(merged) +} + +func mergeJSON(anySource any, anyDestination any) (any, error) { + switch destination := anyDestination.(type) { + case JSONArray: + switch source := anySource.(type) { + case JSONArray: + destination = append(destination, source...) + default: + destination = append(destination, source) + } + return destination, nil + case *JSONObject: + switch source := anySource.(type) { + case *JSONObject: + for _, entry := range source.Entries() { + oldValue, loaded := destination.Get(entry.Key) + if loaded { + var err error + entry.Value, err = mergeJSON(entry.Value, oldValue) + if err != nil { + return nil, E.Cause(err, "merge object item ", entry.Key) + } + } + destination.Put(entry.Key, entry.Value) + } + default: + return nil, E.New("cannot merge json object into ", reflect.TypeOf(destination)) + } + return destination, nil + default: + return destination, nil + } +} diff --git a/common/json/badjson/object.go b/common/json/badjson/object.go new file mode 100644 index 0000000..5d36fa4 --- /dev/null +++ b/common/json/badjson/object.go @@ -0,0 +1,79 @@ +package badjson + +import ( + "bytes" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/linkedhashmap" +) + +type JSONObject struct { + linkedhashmap.Map[string, any] +} + +func (m JSONObject) MarshalJSON() ([]byte, error) { + buffer := new(bytes.Buffer) + buffer.WriteString("{") + items := m.Entries() + iLen := len(items) + for i, entry := range items { + keyContent, err := json.Marshal(entry.Key) + if err != nil { + return nil, err + } + buffer.WriteString(strings.TrimSpace(string(keyContent))) + buffer.WriteString(": ") + valueContent, err := json.Marshal(entry.Value) + if err != nil { + return nil, err + } + buffer.WriteString(strings.TrimSpace(string(valueContent))) + if i < iLen-1 { + buffer.WriteString(", ") + } + } + buffer.WriteString("}") + return buffer.Bytes(), nil +} + +func (m *JSONObject) UnmarshalJSON(content []byte) error { + decoder := json.NewDecoder(bytes.NewReader(content)) + m.Clear() + 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) + } + err = m.decodeJSON(decoder) + if err != nil { + return E.Cause(err, "decode json object content") + } + 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 +} + +func (m *JSONObject) decodeJSON(decoder *json.Decoder) error { + for decoder.More() { + var entryKey string + keyToken, err := decoder.Token() + if err != nil { + return err + } + entryKey = keyToken.(string) + var entryValue any + entryValue, err = decodeJSON(decoder) + if err != nil { + return E.Cause(err, "decode value for ", entryKey) + } + m.Put(entryKey, entryValue) + } + return nil +} diff --git a/common/json/badjson/typed.go b/common/json/badjson/typed.go new file mode 100644 index 0000000..0f83303 --- /dev/null +++ b/common/json/badjson/typed.go @@ -0,0 +1,79 @@ +package badjson + +import ( + "bytes" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/linkedhashmap" +) + +type TypedMap[T any] struct { + linkedhashmap.Map[string, T] +} + +func (m TypedMap[T]) MarshalJSON() ([]byte, error) { + buffer := new(bytes.Buffer) + buffer.WriteString("{") + items := m.Entries() + iLen := len(items) + for i, entry := range items { + keyContent, err := json.Marshal(entry.Key) + if err != nil { + return nil, err + } + buffer.WriteString(strings.TrimSpace(string(keyContent))) + buffer.WriteString(": ") + valueContent, err := json.Marshal(entry.Value) + if err != nil { + return nil, err + } + buffer.WriteString(strings.TrimSpace(string(valueContent))) + if i < iLen-1 { + buffer.WriteString(", ") + } + } + buffer.WriteString("}") + return buffer.Bytes(), nil +} + +func (m *TypedMap[T]) UnmarshalJSON(content []byte) error { + decoder := json.NewDecoder(bytes.NewReader(content)) + m.Clear() + 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) + } + err = m.decodeJSON(decoder) + if err != nil { + return E.Cause(err, "decode json object content") + } + 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 +} + +func (m *TypedMap[T]) decodeJSON(decoder *json.Decoder) error { + for decoder.More() { + var entryKey string + keyToken, err := decoder.Token() + if err != nil { + return err + } + entryKey = keyToken.(string) + var entryValue T + err = decoder.Decode(&entryValue) + if err != nil { + return err + } + m.Put(entryKey, entryValue) + } + return nil +} diff --git a/common/json/comment.go b/common/json/comment.go new file mode 100644 index 0000000..6f3be26 --- /dev/null +++ b/common/json/comment.go @@ -0,0 +1,128 @@ +package json + +import ( + "bufio" + "io" +) + +// kanged from v2ray + +type commentFilterState = byte + +const ( + commentFilterStateContent commentFilterState = iota + commentFilterStateEscape + commentFilterStateDoubleQuote + commentFilterStateDoubleQuoteEscape + commentFilterStateSingleQuote + commentFilterStateSingleQuoteEscape + commentFilterStateComment + commentFilterStateSlash + commentFilterStateMultilineComment + commentFilterStateMultilineCommentStar +) + +type CommentFilter struct { + br *bufio.Reader + state commentFilterState +} + +func NewCommentFilter(reader io.Reader) io.Reader { + return &CommentFilter{br: bufio.NewReader(reader)} +} + +func (v *CommentFilter) Read(b []byte) (int, error) { + p := b[:0] + for len(p) < len(b)-2 { + x, err := v.br.ReadByte() + if err != nil { + if len(p) == 0 { + return 0, err + } + return len(p), nil + } + switch v.state { + case commentFilterStateContent: + switch x { + case '"': + v.state = commentFilterStateDoubleQuote + p = append(p, x) + case '\'': + v.state = commentFilterStateSingleQuote + p = append(p, x) + case '\\': + v.state = commentFilterStateEscape + case '#': + v.state = commentFilterStateComment + case '/': + v.state = commentFilterStateSlash + default: + p = append(p, x) + } + case commentFilterStateEscape: + p = append(p, '\\', x) + v.state = commentFilterStateContent + case commentFilterStateDoubleQuote: + switch x { + case '"': + v.state = commentFilterStateContent + p = append(p, x) + case '\\': + v.state = commentFilterStateDoubleQuoteEscape + default: + p = append(p, x) + } + case commentFilterStateDoubleQuoteEscape: + p = append(p, '\\', x) + v.state = commentFilterStateDoubleQuote + case commentFilterStateSingleQuote: + switch x { + case '\'': + v.state = commentFilterStateContent + p = append(p, x) + case '\\': + v.state = commentFilterStateSingleQuoteEscape + default: + p = append(p, x) + } + case commentFilterStateSingleQuoteEscape: + p = append(p, '\\', x) + v.state = commentFilterStateSingleQuote + case commentFilterStateComment: + if x == '\n' { + v.state = commentFilterStateContent + p = append(p, '\n') + } + case commentFilterStateSlash: + switch x { + case '/': + v.state = commentFilterStateComment + case '*': + v.state = commentFilterStateMultilineComment + default: + p = append(p, '/', x) + } + case commentFilterStateMultilineComment: + switch x { + case '*': + v.state = commentFilterStateMultilineCommentStar + case '\n': + p = append(p, '\n') + } + case commentFilterStateMultilineCommentStar: + switch x { + case '/': + v.state = commentFilterStateContent + case '*': + // Stay + case '\n': + p = append(p, '\n') + default: + v.state = commentFilterStateMultilineComment + } + default: + panic("Unknown state.") + } + } + return len(p), nil +} diff --git a/common/json/context.go b/common/json/context.go new file mode 100644 index 0000000..b68d571 --- /dev/null +++ b/common/json/context.go @@ -0,0 +1,23 @@ +//go:build go1.21 && !without_contextjson + +package json + +import ( + "github.com/sagernet/sing/common/json/internal/contextjson" +) + +var ( + Marshal = json.Marshal + Unmarshal = json.Unmarshal + NewEncoder = json.NewEncoder + NewDecoder = json.NewDecoder +) + +type ( + Encoder = json.Encoder + Decoder = json.Decoder + Token = json.Token + Delim = json.Delim + SyntaxError = json.SyntaxError + RawMessage = json.RawMessage +) diff --git a/common/json/std.go b/common/json/std.go new file mode 100644 index 0000000..e9280dc --- /dev/null +++ b/common/json/std.go @@ -0,0 +1,21 @@ +//go:build !go1.21 || without_contextjson + +package json + +import "encoding/json" + +var ( + Marshal = json.Marshal + Unmarshal = json.Unmarshal + NewEncoder = json.NewEncoder + NewDecoder = json.NewDecoder +) + +type ( + Encoder = json.Encoder + Decoder = json.Decoder + Token = json.Token + Delim = json.Delim + SyntaxError = json.SyntaxError + RawMessage = json.RawMessage +)