formats support

This commit is contained in:
jebbs 2022-10-13 13:05:36 +08:00
parent 6591dd58ca
commit 8145161f3c
24 changed files with 1419 additions and 41 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,6 +3,7 @@ package main
import (
"os"
"github.com/sagernet/sing-box/common/conf/mergers"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
@ -10,9 +11,11 @@ import (
)
var (
configPath string
workingDir string
disableColor bool
configPaths []string
configFormat string
configRecursive bool
workingDir string
disableColor bool
)
var mainCommand = &cobra.Command{
@ -21,7 +24,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

@ -20,6 +20,7 @@ require (
github.com/mholt/acmez v1.0.4
github.com/miekg/dns v1.1.50
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/refraction-networking/utls v1.1.2
github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb
@ -34,13 +35,14 @@ require (
github.com/stretchr/testify v1.8.0
go.etcd.io/bbolt v1.3.6
go.uber.org/atomic v1.10.0
go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0
golang.org/x/net v0.0.0-20221004154528-8021a29435af
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875
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-20220901235040-6ca97ef2ce1c
)
@ -77,7 +79,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

27
go.sum
View File

@ -15,12 +15,16 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=
github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
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/cloudflare/circl v1.2.1-0.20220831060716-4cf0150356fc h1:307gdRLiZ08dwOIKwc5lAQ19DRFaQQvdhHalyB4Asx8=
github.com/cloudflare/circl v1.2.1-0.20220831060716-4cf0150356fc/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
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/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
@ -36,7 +40,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=
@ -124,6 +128,8 @@ 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/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -188,16 +194,16 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab h1:+yW1yrZ09EYNu1spCUOHBBNRbrLnfmutwyhbhCv3b6Q=
go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc=
go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d h1:ggxwEf5eu0l8v+87VhX1czFh8zJul3hK16Gmruxn7hw=
go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
@ -205,8 +211,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=
@ -284,7 +288,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=
@ -305,15 +308,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=