mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
formats support
This commit is contained in:
parent
3105b8c920
commit
c5d0adba9d
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
72
common/conf/conf.go
Normal 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
83
common/conf/fs.go
Normal 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
|
||||
}
|
57
common/conf/jsonc/decode.go
Normal file
57
common/conf/jsonc/decode.go
Normal 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
|
||||
}
|
45
common/conf/jsonc/decode_test.go
Normal file
45
common/conf/jsonc/decode_test.go
Normal 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
129
common/conf/jsonc/reader.go
Normal 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
|
||||
}
|
100
common/conf/jsonc/reader_test.go
Normal file
100
common/conf/jsonc/reader_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
37
common/conf/merge/convert.go
Normal file
37
common/conf/merge/convert.go
Normal 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
55
common/conf/merge/map.go
Normal 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
|
||||
}
|
189
common/conf/merge/merge_test.go
Normal file
189
common/conf/merge/merge_test.go
Normal 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))
|
||||
}
|
||||
}
|
31
common/conf/merge/priority.go
Normal file
31
common/conf/merge/priority.go
Normal 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])
|
||||
},
|
||||
)
|
||||
}
|
56
common/conf/merge/rules.go
Normal file
56
common/conf/merge/rules.go
Normal 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
53
common/conf/merge/tag.go
Normal 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
|
||||
}
|
26
common/conf/mergers/extensions.go
Normal file
26
common/conf/mergers/extensions.go
Normal 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
|
||||
}
|
29
common/conf/mergers/formats.go
Normal file
29
common/conf/mergers/formats.go
Normal 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
|
||||
}
|
||||
}
|
62
common/conf/mergers/load.go
Normal file
62
common/conf/mergers/load.go
Normal 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
|
||||
}
|
89
common/conf/mergers/merge.go
Normal file
89
common/conf/mergers/merge.go
Normal 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)
|
||||
}
|
100
common/conf/mergers/merger_base.go
Normal file
100
common/conf/mergers/merger_base.go
Normal 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)
|
||||
}
|
91
common/conf/mergers/mergers.go
Normal file
91
common/conf/mergers/mergers.go
Normal 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
50
common/conf/output.go
Normal 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
8
go.mod
@ -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
35
go.sum
@ -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=
|
||||
|
Loading…
x
Reference in New Issue
Block a user