formats support

This commit is contained in:
jebbs 2022-10-13 13:05:36 +08:00
parent 3105b8c920
commit c5d0adba9d
24 changed files with 1428 additions and 40 deletions

View File

@ -1,11 +1,11 @@
package main
import (
"bytes"
"os"
"path/filepath"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/common/conf"
"github.com/sagernet/sing-box/common/conf/mergers"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
@ -13,7 +13,8 @@ import (
"github.com/spf13/cobra"
)
var commandFormatFlagWrite bool
var commandFormatWrite string
var commandEncodeFormat string
var commandFormat = &cobra.Command{
Use: "format",
@ -28,44 +29,55 @@ var commandFormat = &cobra.Command{
}
func init() {
commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
commandFormat.Flags().StringVarP(&commandFormatWrite, "write", "w", "", "write result to (source) file instead of stdout")
commandFormat.Flags().StringVarP(&commandEncodeFormat, "encode", "e", string(mergers.FormatJSON), "encode format")
mainCommand.AddCommand(commandFormat)
}
func format() error {
configContent, err := os.ReadFile(configPath)
var (
configContent []byte
err error
)
format := mergers.ParseFormat(configFormat)
encode := mergers.ParseFormat(commandEncodeFormat)
if encode == mergers.FormatAuto {
encode = mergers.FormatJSON
}
if len(configPaths) == 1 && configPaths[0] == "stdin" {
configContent, err = conf.ReaderToJSON(os.Stdin, format)
} else {
configContent, err = conf.FilesToJSON(configPaths, format, configRecursive)
}
if err != nil {
return E.Cause(err, "read config")
}
var options option.Options
err = options.UnmarshalJSON(configContent)
if err != nil {
return E.Cause(err, "decode config")
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(options)
content, err := conf.Sprint(options, encode)
if err != nil {
return E.Cause(err, "encode config")
}
if !commandFormatFlagWrite {
os.Stdout.WriteString(buffer.String() + "\n")
if commandFormatWrite == "" {
os.Stdout.WriteString(content + "\n")
return nil
}
if bytes.Equal(configContent, buffer.Bytes()) {
return nil
}
output, err := os.Create(configPath)
output, err := os.Create(commandFormatWrite)
if err != nil {
return E.Cause(err, "open output")
}
_, err = output.Write(buffer.Bytes())
_, err = output.WriteString(content)
output.Close()
if err != nil {
return E.Cause(err, "write output")
}
outputPath, _ := filepath.Abs(configPath)
outputPath, _ := filepath.Abs(commandFormatWrite)
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

@ -2,13 +2,14 @@ package main
import (
"context"
"io"
"os"
"os/signal"
runtimeDebug "runtime/debug"
"syscall"
"github.com/sagernet/sing-box"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/common/conf"
"github.com/sagernet/sing-box/common/conf/mergers"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
@ -36,10 +37,11 @@ func readConfig() (option.Options, error) {
configContent []byte
err error
)
if configPath == "stdin" {
configContent, err = io.ReadAll(os.Stdin)
format := mergers.ParseFormat(configFormat)
if len(configPaths) == 1 && configPaths[0] == "stdin" {
configContent, err = conf.ReaderToJSON(os.Stdin, format)
} else {
configContent, err = os.ReadFile(configPath)
configContent, err = conf.FilesToJSON(configPaths, format, configRecursive)
}
if err != nil {
return option.Options{}, E.Cause(err, "read config")

View File

@ -3,15 +3,18 @@ package main
import (
"os"
"github.com/sagernet/sing-box/common/conf/mergers"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var (
configPath string
workingDir string
disableColor bool
configPaths []string
configFormat string
configRecursive bool
workingDir string
disableColor bool
)
var mainCommand = &cobra.Command{
@ -20,7 +23,9 @@ var mainCommand = &cobra.Command{
}
func init() {
mainCommand.PersistentFlags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path")
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", []string{"config.json"}, "set configuration file path")
mainCommand.PersistentFlags().StringVarP(&configFormat, "format", "", string(mergers.FormatAuto), "load configuration directories recursively")
mainCommand.PersistentFlags().BoolVarP(&configRecursive, "recursive", "r", false, "load configuration directories recursively")
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
}

72
common/conf/conf.go Normal file
View File

@ -0,0 +1,72 @@
package conf
import (
"encoding/json"
"io"
"github.com/sagernet/sing-box/common/conf/merge"
"github.com/sagernet/sing-box/common/conf/mergers"
)
// Reader loads json data to v.
func Reader(v interface{}, r io.Reader, format mergers.Format) error {
data, err := ReaderToJSON(r, format)
if err != nil {
return err
}
return json.Unmarshal(data, v)
}
// Files load config files to v.
// it will resolve folder to files
func Files(v interface{}, files []string, format mergers.Format, recursively bool) error {
data, err := FilesToJSON(files, format, recursively)
if err != nil {
return err
}
return json.Unmarshal(data, v)
}
// ReaderToJSON load reader content as JSON bytes.
func ReaderToJSON(r io.Reader, format mergers.Format) ([]byte, error) {
m := make(map[string]interface{})
err := mergers.MergeAs(format, r, m)
if err != nil {
return nil, err
}
err = merge.ApplyRules(m)
if err != nil {
return nil, err
}
merge.RemoveHelperFields(m)
return json.Marshal(m)
}
// FilesToJSON merges config files JSON bytes
func FilesToJSON(files []string, format mergers.Format, recursively bool) ([]byte, error) {
var err error
if len(files) > 0 {
var extensions []string
extensions, err := mergers.GetExtensions(format)
if err != nil {
return nil, err
}
files, err = resolveFolderToFiles(files, extensions, recursively)
if err != nil {
return nil, err
}
}
m := make(map[string]interface{})
if len(files) > 0 {
err = mergers.MergeAs(format, files, m)
if err != nil {
return nil, err
}
}
err = merge.ApplyRules(m)
if err != nil {
return nil, err
}
merge.RemoveHelperFields(m)
return json.Marshal(m)
}

83
common/conf/fs.go Normal file
View File

@ -0,0 +1,83 @@
package conf
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
)
// resolveFolderToFiles expands folder path (if any and it exists) to file paths.
// Any other paths, like file, even URL, it returns them as is.
func resolveFolderToFiles(paths []string, extensions []string, recursively bool) ([]string, error) {
dirReader := readDir
if recursively {
dirReader = readDirRecursively
}
files := make([]string, 0)
for _, p := range paths {
i, err := os.Stat(p)
// don't raise error for net url
if err != nil && isPathOnly(p) {
return nil, err
}
if err == nil && i.IsDir() {
fs, err := dirReader(p, extensions)
if err != nil {
return nil, fmt.Errorf("failed to read dir %s: %s", p, err)
}
files = append(files, fs...)
continue
}
files = append(files, p)
}
return files, nil
}
// readDir finds files according to extensions in the dir
func readDir(dir string, extensions []string) ([]string, error) {
confs, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
files := make([]string, 0)
for _, f := range confs {
ext := filepath.Ext(f.Name())
for _, e := range extensions {
if strings.EqualFold(ext, e) {
files = append(files, filepath.Join(dir, f.Name()))
break
}
}
}
return files, nil
}
// readDirRecursively finds files according to extensions in the dir recursively
func readDirRecursively(dir string, extensions []string) ([]string, error) {
files := make([]string, 0)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
ext := filepath.Ext(path)
for _, e := range extensions {
if strings.EqualFold(ext, e) {
files = append(files, path)
break
}
}
return nil
})
if err != nil {
return nil, err
}
return files, nil
}
func isPathOnly(p string) bool {
u, err := url.Parse(p)
if err != nil {
return false
}
return u.Path == p
}

View File

@ -0,0 +1,57 @@
package jsonc
import (
"bytes"
"encoding/json"
"fmt"
"io"
)
type offset struct {
line int
char int
}
func findOffset(b []byte, o int) *offset {
if o >= len(b) || o < 0 {
return nil
}
line := 1
char := 0
for i, x := range b {
if i == o {
break
}
if x == '\n' {
line++
char = 0
} else {
char++
}
}
return &offset{line: line, char: char}
}
// Decode reads from reader and decode into target
// syntax error could be detected.
func Decode(reader io.Reader, target interface{}) error {
jsonContent := bytes.NewBuffer(make([]byte, 0, 10240))
jsonReader := io.TeeReader(&Reader{
Reader: reader,
}, jsonContent)
decoder := json.NewDecoder(jsonReader)
if err := decoder.Decode(target); err != nil {
var pos *offset
if tErr, ok := err.(*json.SyntaxError); ok {
pos = findOffset(jsonContent.Bytes(), int(tErr.Offset))
} else if tErr, ok := err.(*json.UnmarshalTypeError); ok {
pos = findOffset(jsonContent.Bytes(), int(tErr.Offset))
}
if pos != nil {
return fmt.Errorf("failed to decode jsonc at line %d char %d: %s", pos.line, pos.char, err)
}
return fmt.Errorf("failed to decode jsonc: %s", err)
}
return nil
}

View File

@ -0,0 +1,45 @@
package jsonc_test
import (
"bytes"
"strings"
"testing"
"github.com/sagernet/sing-box/common/conf/jsonc"
)
func TestLoaderError(t *testing.T) {
testCases := []struct {
Input string
Output string
}{
{
Input: `{
"log": {
// abcd
0,
"loglevel": "info"
}
}`,
Output: "line 4 char 6",
},
{
Input: `{
"log": {
// abcd
"loglevel": "info",
}
}`,
Output: "line 5 char 5",
},
}
for _, testCase := range testCases {
reader := bytes.NewReader([]byte(testCase.Input))
m := make(map[string]interface{})
err := jsonc.Decode(reader, m)
errString := err.Error()
if !strings.Contains(errString, testCase.Output) {
t.Error("unexpected output from json: ", testCase.Input, ". expected ", testCase.Output, ", but actually ", errString)
}
}
}

129
common/conf/jsonc/reader.go Normal file
View File

@ -0,0 +1,129 @@
package jsonc
import (
"io"
)
// State is the internal state of parser.
type State byte
// states
const (
StateContent State = iota
StateEscape
StateDoubleQuote
StateDoubleQuoteEscape
StateSingleQuote
StateSingleQuoteEscape
StateComment
StateSlash
StateMultilineComment
StateMultilineCommentStar
)
// Reader is a reader for filtering comments.
// It supports Java style single and multi line comment syntax, and Python style single line comment syntax.
type Reader struct {
io.Reader
state State
}
// Read implements io.Reader.Read()
func (v *Reader) Read(b []byte) (int, error) {
var sb [1]byte
p := b[:0]
for len(p) < len(b)-2 {
_, err := v.Reader.Read(sb[:])
x := sb[0]
if err != nil {
if len(p) == 0 {
return 0, err
}
return len(p), nil
}
switch v.state {
case StateContent:
switch x {
case '"':
v.state = StateDoubleQuote
p = append(p, x)
case '\'':
v.state = StateSingleQuote
p = append(p, x)
case '\\':
v.state = StateEscape
case '#':
v.state = StateComment
case '/':
v.state = StateSlash
default:
p = append(p, x)
}
case StateEscape:
p = append(p, '\\', x)
v.state = StateContent
case StateDoubleQuote:
switch x {
case '"':
v.state = StateContent
p = append(p, x)
case '\\':
v.state = StateDoubleQuoteEscape
default:
p = append(p, x)
}
case StateDoubleQuoteEscape:
p = append(p, '\\', x)
v.state = StateDoubleQuote
case StateSingleQuote:
switch x {
case '\'':
v.state = StateContent
p = append(p, x)
case '\\':
v.state = StateSingleQuoteEscape
default:
p = append(p, x)
}
case StateSingleQuoteEscape:
p = append(p, '\\', x)
v.state = StateSingleQuote
case StateComment:
if x == '\n' {
v.state = StateContent
p = append(p, '\n')
}
case StateSlash:
switch x {
case '/':
v.state = StateComment
case '*':
v.state = StateMultilineComment
default:
p = append(p, '/', x)
}
case StateMultilineComment:
switch x {
case '*':
v.state = StateMultilineCommentStar
case '\n':
p = append(p, '\n')
}
case StateMultilineCommentStar:
switch x {
case '/':
v.state = StateContent
case '*':
// Stay
case '\n':
p = append(p, '\n')
default:
v.state = StateMultilineComment
}
default:
panic("Unknown state.")
}
}
return len(p), nil
}

View File

@ -0,0 +1,100 @@
package jsonc_test
import (
"bytes"
"io"
"testing"
"github.com/sagernet/sing-box/common/conf/jsonc"
)
func TestReader(t *testing.T) {
data := []struct {
input string
output string
}{
{
`
content #comment 1
#comment 2
content 2`,
`
content
content 2`},
{`content`, `content`},
{" ", " "},
{`con/*abcd*/tent`, "content"},
{`
text // adlkhdf /*
//comment adfkj
text 2*/`, `
text
text 2*`},
{`"//"content`, `"//"content`},
{`abcd'//'abcd`, `abcd'//'abcd`},
{`"\""`, `"\""`},
{`\"/*abcd*/\"`, `\"\"`},
}
for _, testCase := range data {
reader := &jsonc.Reader{
Reader: bytes.NewReader([]byte(testCase.input)),
}
buf := make([]byte, 1024)
n, err := reader.Read(buf)
if err != nil {
t.Error(err)
return
}
got := buf[:n]
if string(got) != testCase.output {
t.Errorf("want: %s, got: %s", testCase.output, got)
return
}
}
}
func TestReader1(t *testing.T) {
type dataStruct struct {
input string
output string
}
bufLen := 8
data := []dataStruct{
{"loooooooooooooooooooooooooooooooooooooooog", "loooooooooooooooooooooooooooooooooooooooog"},
{`{"t": "\/testlooooooooooooooooooooooooooooong"}`, `{"t": "\/testlooooooooooooooooooooooooooooong"}`},
{`{"t": "\/test"}`, `{"t": "\/test"}`},
{`"\// fake comment"`, `"\// fake comment"`},
{`"\/\/\/\/\/"`, `"\/\/\/\/\/"`},
}
for _, testCase := range data {
reader := &jsonc.Reader{
Reader: bytes.NewReader([]byte(testCase.input)),
}
target := make([]byte, 0)
buf := make([]byte, bufLen)
var n int
var err error
for n, err = reader.Read(buf); err == nil; n, err = reader.Read(buf) {
if n > len(buf) {
t.Error("n: ", n)
}
target = append(target, buf[:n]...)
buf = make([]byte, bufLen)
}
if err != nil && err != io.EOF {
t.Error("error: ", err)
}
got := string(target)
if string(got) != testCase.output {
t.Errorf("want: %s, got: %s", testCase.output, got)
return
}
}
}

View File

@ -0,0 +1,37 @@
package merge
import (
"fmt"
)
// Convert converts map[interface{}]interface{} to map[string]interface{} which
// is mergable by merge.Maps
func Convert(m map[interface{}]interface{}) map[string]interface{} {
return convert(m)
}
func convert(m map[interface{}]interface{}) map[string]interface{} {
res := map[string]interface{}{}
for k, v := range m {
var value interface{}
switch v2 := v.(type) {
case map[interface{}]interface{}:
value = convert(v2)
case []interface{}:
for i, el := range v2 {
if m, ok := el.(map[interface{}]interface{}); ok {
v2[i] = convert(m)
}
}
value = v2
default:
value = v
}
key := "null"
if k != nil {
key = fmt.Sprint(k)
}
res[key] = value
}
return res
}

55
common/conf/merge/map.go Normal file
View File

@ -0,0 +1,55 @@
// Copyright 2020 Jebbs. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package merge
import (
"fmt"
)
// Maps merges source maps into target
func Maps(target map[string]interface{}, sources ...map[string]interface{}) (err error) {
for _, source := range sources {
err = mergeMaps(target, source)
if err != nil {
return err
}
}
return nil
}
// mergeMaps merges source map into target
// it supports only map[string]interface{} type for any children of the map tree
func mergeMaps(target map[string]interface{}, source map[string]interface{}) (err error) {
for key, value := range source {
target[key], err = mergeField(target[key], value)
if err != nil {
return
}
}
return
}
func mergeField(target interface{}, source interface{}) (interface{}, error) {
if source == nil {
return target, nil
}
if target == nil {
return source, nil
}
if slice, ok := source.([]interface{}); ok {
if tslice, ok := target.([]interface{}); ok {
tslice = append(tslice, slice...)
return tslice, nil
}
return nil, fmt.Errorf("value type mismatch, source is 'slice' but target not: %s", source)
} else if smap, ok := source.(map[string]interface{}); ok {
if tmap, ok := target.(map[string]interface{}); ok {
err := mergeMaps(tmap, smap)
return tmap, err
}
return nil, fmt.Errorf("value type mismatch, source is 'map[string]interface{}' but target not: %s", source)
}
return source, nil
}

View File

@ -0,0 +1,189 @@
// Copyright 2020 Jebbs. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package merge_test
import (
"encoding/json"
"reflect"
"strings"
"testing"
"github.com/sagernet/sing-box/common/conf/jsonc"
"github.com/sagernet/sing-box/common/conf/merge"
)
func TestMergeV2Style(t *testing.T) {
json1 := `
{
"log": {"level": "debug"},
"nodeA": [{"_tag": "a1","value": "a1"}],
"nodeB": [{"_priority": 100, "_tag": "b1","value": "b1"}],
"nodeC": [
{"_tag":"c1","aTag":["a1"],"bTag":"b1"}
]
}
`
json2 := `
{
"log": {"level": "error"},
"nodeA": [{"_tag": "a2","value": "a2"}],
"nodeB": [{"_priority": -100, "_tag": "b2","value": "b2"}],
"nodeC": [
{"aTag":["a2"],"bTag":"b2"},
{"_tag":"c1","aTag":["a1.1"],"bTag":"b1.1"}
]
}
`
expected := `
{
// level is overwritten
"log": {"level": "error"},
"nodeA": [{"value": "a1"}, {"value": "a2"}],
"nodeB": [
{"value": "b2"}, // the order is affected by priority
{"value": "b1"}
],
"nodeC": [
// 3 items are merged into 2, and bTag is overwritten,
// because 2 of them has same tag
{"aTag":["a1","a1.1"],"bTag":"b1.1"},
{"aTag":["a2"],"bTag":"b2"}
]
}
`
m, err := jsonsToMap(json1, json2)
if err != nil {
t.Error(err)
}
assertResult(t, m, expected)
}
func TestMergeTagValueTypes(t *testing.T) {
json1 := `
{
"array_1": [{
"_tag":"1",
"array_2": [{
"_tag":"2",
"array_3.1": ["string",true,false],
"array_3.2": [1,2,3],
"number_1": 1,
"number_2": 1,
"bool_1": true,
"bool_2": true
}]
}]
}
`
json2 := `
{
"array_1": [{
"_tag":"1",
"array_2": [{
"_tag":"2",
"array_3.1": [0,1,null],
"array_3.2": null,
"number_1": 0,
"number_2": 1,
"bool_1": true,
"bool_2": false,
"null_1": null
}]
}]
}
`
expected := `
{
"array_1": [{
"array_2": [{
"array_3.1": ["string",true,false,0,1,null],
"array_3.2": [1,2,3],
"number_1": 0,
"number_2": 1,
"bool_1": true,
"bool_2": false,
"null_1": null
}]
}]
}
`
m, err := jsonsToMap(json1, json2)
if err != nil {
t.Error(err)
}
assertResult(t, m, expected)
}
func TestMergeTagDeep(t *testing.T) {
json1 := `
{
"array_1": [{
"_tag":"1",
"array_2": [{
"_tag":"2",
"array_3": [true,false,"string"]
}]
}]
}
`
json2 := `
{
"array_1": [{
"_tag":"1",
"array_2": [{
"_tag":"2",
"_priority":-100,
"array_3": [0,1,null]
}]
}]
}
`
expected := `
{
"array_1": [{
"array_2": [{
"array_3": [0,1,null,true,false,"string"]
}]
}]
}
`
m, err := jsonsToMap(json1, json2)
if err != nil {
t.Error(err)
}
assertResult(t, m, expected)
}
func jsonsToMap(jsonStrs ...string) (map[string]interface{}, error) {
merged := make(map[string]interface{})
maps := make([]map[string]interface{}, len(jsonStrs))
for _, j := range jsonStrs {
m := map[string]interface{}{}
json.Unmarshal([]byte(j), &m)
maps = append(maps, m)
}
err := merge.Maps(merged, maps...)
if err != nil {
return nil, err
}
err = merge.ApplyRules(merged)
if err != nil {
return nil, err
}
merge.RemoveHelperFields(merged)
return merged, nil
}
func assertResult(t *testing.T, got map[string]interface{}, want string) {
e := make(map[string]interface{})
err := jsonc.Decode(strings.NewReader(want), &e)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(got, e) {
b, _ := json.Marshal(got)
t.Fatalf("want:\n%s\n\ngot:\n%s", want, string(b))
}
}

View File

@ -0,0 +1,31 @@
// Copyright 2020 Jebbs. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package merge
import "sort"
func getPriority(v interface{}) float64 {
var m map[string]interface{}
var ok bool
if m, ok = v.(map[string]interface{}); !ok {
return 0
}
if i, ok := m[priorityKey]; ok {
if p, ok := i.(float64); ok {
return p
}
}
return 0
}
// sortByPriority sort slice by priority fields of their elements
func sortByPriority(slice []interface{}) {
sort.Slice(
slice,
func(i, j int) bool {
return getPriority(slice[i]) < getPriority(slice[j])
},
)
}

View File

@ -0,0 +1,56 @@
// Copyright 2020 Jebbs. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package merge
const priorityKey string = "_priority"
const tagKey string = "_tag"
// ApplyRules applies merge rules according to _tag, _priority fields
func ApplyRules(m map[string]interface{}) error {
return sortMergeSlices(m)
}
// RemoveHelperFields removes helper fields from a map, including "_priority" and "_tag"
func RemoveHelperFields(target map[string]interface{}) {
removeHelperFields(target)
}
// sortMergeSlices enumerates all slices in a map, to sort by priority and merge by tag
func sortMergeSlices(target map[string]interface{}) error {
for key, value := range target {
if slice, ok := value.([]interface{}); ok {
sortByPriority(slice)
s, err := mergeSameTag(slice)
if err != nil {
return err
}
target[key] = s
for _, item := range s {
if m, ok := item.(map[string]interface{}); ok {
sortMergeSlices(m)
}
}
} else if field, ok := value.(map[string]interface{}); ok {
sortMergeSlices(field)
}
}
return nil
}
func removeHelperFields(target map[string]interface{}) {
for key, value := range target {
if key == priorityKey || key == tagKey {
delete(target, key)
} else if slice, ok := value.([]interface{}); ok {
for _, e := range slice {
if el, ok := e.(map[string]interface{}); ok {
removeHelperFields(el)
}
}
} else if field, ok := value.(map[string]interface{}); ok {
removeHelperFields(field)
}
}
}

53
common/conf/merge/tag.go Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2020 Jebbs. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package merge
func getTag(v map[string]interface{}) string {
if field, ok := v[tagKey]; ok {
if t, ok := field.(string); ok {
return t
}
}
return ""
}
func mergeSameTag(s []interface{}) ([]interface{}, error) {
// from: [a,"",b,"",a,"",b,""]
// to: [a,"",b,"",merged,"",merged,""]
merged := &struct{}{}
for i, item1 := range s {
map1, ok := item1.(map[string]interface{})
if !ok {
continue
}
tag1 := getTag(map1)
if tag1 == "" {
continue
}
for j := i + 1; j < len(s); j++ {
map2, ok := s[j].(map[string]interface{})
if !ok {
continue
}
tag2 := getTag(map2)
if tag1 == tag2 {
s[j] = merged
err := mergeMaps(map1, map2)
if err != nil {
return nil, err
}
}
}
}
// remove merged
ns := make([]interface{}, 0)
for _, item := range s {
if item == merged {
continue
}
ns = append(ns, item)
}
return ns, nil
}

View File

@ -0,0 +1,26 @@
package mergers
import (
"fmt"
)
// GetExtensions get extensions of given format
func GetExtensions(formatName Format) ([]string, error) {
if formatName == FormatAuto {
return GetAllExtensions(), nil
}
f, found := mergersByName[formatName]
if !found {
return nil, fmt.Errorf("%s not found", formatName)
}
return f.Extensions, nil
}
// GetAllExtensions get all extensions supported
func GetAllExtensions() []string {
extensions := make([]string, 0)
for _, f := range mergersByName {
extensions = append(extensions, f.Extensions...)
}
return extensions
}

View File

@ -0,0 +1,29 @@
package mergers
import (
"strings"
)
// Format is the supported format of mergers
type Format string
// Supported formats
const (
FormatAuto Format = "auto"
FormatJSON Format = "json"
FormatJSONC Format = "jsonc"
FormatTOML Format = "toml"
FormatYAML Format = "yaml"
)
// ParseFormat parses format from name, it returns FormatAuto if fails.
func ParseFormat(name string) Format {
name = strings.ToLower(strings.TrimSpace(name))
format := Format(name)
switch format {
case FormatAuto, FormatJSON, FormatJSONC, FormatTOML, FormatYAML:
return format
default:
return FormatAuto
}
}

View File

@ -0,0 +1,62 @@
package mergers
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
// loadToBytes loads one arg to []byte, maybe an remote url, or local file path
func loadToBytes(arg string) (out []byte, err error) {
switch {
case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"):
out, err = fetchHTTPContent(arg)
default:
out, err = ioutil.ReadFile(arg)
}
if err != nil {
return
}
return
}
// fetchHTTPContent dials https for remote content
func fetchHTTPContent(target string) ([]byte, error) {
parsedTarget, err := url.Parse(target)
if err != nil {
return nil, fmt.Errorf("invalid URL: %s", target)
}
if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" {
return nil, fmt.Errorf("invalid scheme: %s", parsedTarget.Scheme)
}
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Do(&http.Request{
Method: "GET",
URL: parsedTarget,
Close: true,
})
if err != nil {
return nil, fmt.Errorf("failed to dial to %s", target)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode)
}
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.New("failed to read HTTP response")
}
return content, nil
}

View File

@ -0,0 +1,89 @@
package mergers
import (
"errors"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
)
// MergeAs load input and merge as specified format into m
func MergeAs(formatName Format, input interface{}, m map[string]interface{}) error {
f, found := mergersByName[formatName]
if !found {
return fmt.Errorf("format merger not found for: %s", formatName)
}
return f.Merge(input, m)
}
// Merge loads inputs and merges them into target
// it detects extension for merger selecting, or try all mergers
// if no extension found
func Merge(input interface{}, target map[string]interface{}) error {
switch v := input.(type) {
case string:
err := mergeSingleFile(v, target)
if err != nil {
return err
}
case []string:
for _, file := range v {
err := mergeSingleFile(file, target)
if err != nil {
return err
}
}
case []byte:
err := mergeSingleFile(v, target)
if err != nil {
return err
}
case io.Reader:
// read to []byte incase it tries different mergers
bs, err := ioutil.ReadAll(v)
if err != nil {
return err
}
err = mergeSingleFile(bs, target)
if err != nil {
return err
}
default:
return errors.New("unknow merge input type")
}
return nil
}
func mergeSingleFile(input interface{}, m map[string]interface{}) error {
if file, ok := input.(string); ok {
ext := getExtension(file)
if ext != "" {
lext := strings.ToLower(ext)
f, found := mergersByExt[lext]
if !found {
return fmt.Errorf("unmergeable format extension: %s", ext)
}
return f.Merge(file, m)
}
}
var errs []string
// no extension, try all mergers
for _, f := range mergersByName {
if f.Name == FormatAuto {
continue
}
err := f.Merge(input, m)
if err == nil {
return nil
}
errs = append(errs, fmt.Sprintf("[%s] %s", f.Name, err))
}
return fmt.Errorf("tried all mergers but failed for: \n\n%s\n\nreason:\n\n %s", input, strings.Join(errs, "\n "))
}
func getExtension(filename string) string {
ext := filepath.Ext(filename)
return strings.ToLower(ext)
}

View File

@ -0,0 +1,100 @@
package mergers
import (
"errors"
"fmt"
"io"
"io/ioutil"
"github.com/sagernet/sing-box/common/conf/merge"
)
// Merger is a configurable format merger for V2Ray config files.
type Merger struct {
Name Format
Extensions []string
Merge mergeFunc
}
// mergeFunc is a utility to merge the input into map[string]interface{}
type mergeFunc func(interface{}, map[string]interface{}) error
// mapFunc converts the input bytes of a config content to map[string]interface{}
type mapFunc func([]byte) (map[string]interface{}, error)
// makeMerger makes a merger who merge the format by converting it to JSON
func makeMerger(name Format, extensions []string, converter mapFunc) *Merger {
return &Merger{
Name: name,
Extensions: extensions,
Merge: makeMergeFunc(converter),
}
}
// makeMergeFunc makes a merge func who merge the input to
func makeMergeFunc(converter mapFunc) mergeFunc {
return func(input interface{}, target map[string]interface{}) error {
if target == nil {
panic("merge target is nil")
}
switch v := input.(type) {
case string:
err := loadFile(v, target, converter)
if err != nil {
return err
}
case []string:
err := loadFiles(v, target, converter)
if err != nil {
return err
}
case []byte:
err := loadBytes(v, target, converter)
if err != nil {
return err
}
case io.Reader:
err := loadReader(v, target, converter)
if err != nil {
return err
}
default:
return errors.New("unknow merge input type")
}
return nil
}
}
func loadFiles(files []string, target map[string]interface{}, converter mapFunc) error {
for _, file := range files {
err := loadFile(file, target, converter)
if err != nil {
return err
}
}
return nil
}
func loadFile(file string, target map[string]interface{}, converter mapFunc) error {
bs, err := loadToBytes(file)
if err != nil {
return fmt.Errorf("fail to load %s: %s", file, err)
}
return loadBytes(bs, target, converter)
}
func loadReader(reader io.Reader, target map[string]interface{}, converter mapFunc) error {
bs, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
return loadBytes(bs, target, converter)
}
func loadBytes(bs []byte, target map[string]interface{}, converter mapFunc) error {
m, err := converter(bs)
if err != nil {
return err
}
return merge.Maps(target, m)
}

View File

@ -0,0 +1,91 @@
package mergers
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/pelletier/go-toml"
"github.com/sagernet/sing-box/common/conf/jsonc"
"github.com/sagernet/sing-box/common/conf/merge"
"github.com/sagernet/sing/common"
"gopkg.in/yaml.v2"
)
func init() {
common.Must(registerMerger(makeMerger(
FormatJSON,
[]string{".json"},
func(v []byte) (map[string]interface{}, error) {
m := make(map[string]interface{})
if err := json.Unmarshal(v, &m); err != nil {
return nil, err
}
return m, nil
},
)))
common.Must(registerMerger(makeMerger(
FormatJSONC,
[]string{".jsonc"},
func(v []byte) (map[string]interface{}, error) {
m := make(map[string]interface{})
if err := jsonc.Decode(bytes.NewReader(v), &m); err != nil {
return nil, err
}
return m, nil
},
)))
common.Must(registerMerger(makeMerger(
FormatTOML,
[]string{".toml"},
func(v []byte) (map[string]interface{}, error) {
m := make(map[string]interface{})
if err := toml.Unmarshal(v, &m); err != nil {
return nil, err
}
return m, nil
},
)))
common.Must(registerMerger(makeMerger(
FormatYAML,
[]string{".yml", ".yaml"},
func(v []byte) (map[string]interface{}, error) {
m1 := make(map[interface{}]interface{})
err := yaml.Unmarshal(v, &m1)
if err != nil {
return nil, err
}
m2 := merge.Convert(m1)
return m2, nil
},
)))
common.Must(registerMerger(
&Merger{
Name: FormatAuto,
Extensions: nil,
Merge: Merge,
}),
)
}
var (
mergersByName = make(map[Format]*Merger)
mergersByExt = make(map[string]*Merger)
)
// registerMerger add a new Merger.
func registerMerger(format *Merger) error {
if _, found := mergersByName[format.Name]; found {
return fmt.Errorf("%s already registered", format.Name)
}
mergersByName[format.Name] = format
for _, ext := range format.Extensions {
lext := strings.ToLower(ext)
if f, found := mergersByExt[lext]; found {
return fmt.Errorf("%s already registered to %s", ext, f.Name)
}
mergersByExt[lext] = format
}
return nil
}

50
common/conf/output.go Normal file
View File

@ -0,0 +1,50 @@
package conf
import (
"bytes"
"encoding/json"
"fmt"
"github.com/pelletier/go-toml"
"github.com/sagernet/sing-box/common/conf/mergers"
"gopkg.in/yaml.v2"
)
// Sprint returns the text of give format for v
func Sprint(v interface{}, format mergers.Format) (string, error) {
var (
out []byte
err error
)
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(v)
if err != nil {
return "", fmt.Errorf("failed to convert to json: %s", err)
}
if format == mergers.FormatJSON || format == mergers.FormatJSONC {
return string(buffer.Bytes()), nil
}
m := make(map[string]interface{})
err = json.Unmarshal(buffer.Bytes(), &m)
if err != nil {
return "", err
}
switch format {
case mergers.FormatTOML:
out, err = toml.Marshal(m)
if err != nil {
return "", fmt.Errorf("failed to convert to toml: %s", err)
}
case mergers.FormatYAML:
out, err = yaml.Marshal(m)
if err != nil {
return "", fmt.Errorf("failed to convert to yaml: %s", err)
}
default:
return "", fmt.Errorf("invalid output format: %s", format)
}
return string(out), nil
}

8
go.mod
View File

@ -16,6 +16,7 @@ require (
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mholt/acmez v1.0.4
github.com/oschwald/maxminddb-golang v1.10.0
github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.6.2
github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a
github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb
@ -31,11 +32,12 @@ require (
go.uber.org/atomic v1.10.0
go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261
golang.zx2c4.com/wireguard v0.0.0-20220829161405-d1d08426b27b
google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v2 v2.4.0
gvisor.dev/gvisor v0.0.0-20220819163037-ba6e795b139a
)
@ -50,6 +52,7 @@ require (
github.com/google/btree v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
@ -57,6 +60,7 @@ require (
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e // indirect
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
github.com/sagernet/netlink v0.0.0-20220826133217-3fb4ff92ea17 // indirect
@ -70,7 +74,7 @@ require (
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect
google.golang.org/genproto v0.0.0-20220323144105-ec3c684e5b14 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.1.7 // indirect

35
go.sum
View File

@ -9,11 +9,16 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
@ -28,7 +33,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@ -82,11 +87,13 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
@ -112,14 +119,20 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34=
github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g=
@ -190,8 +203,6 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
@ -212,8 +223,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -264,7 +275,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
@ -284,15 +294,15 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f h1:YORWxaStkWBnWgELOHTmDrqNlFXuVGEbhwbB5iK94bQ=
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20220323144105-ec3c684e5b14 h1:17TOyVD+9MLIDtDJW9PdtMuVT7gNLEkN+G/xFYjZmr8=
google.golang.org/genproto v0.0.0-20220323144105-ec3c684e5b14/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@ -312,6 +322,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=