Browse Source

Add `rule-set merge` command

世界 10 months ago
parent
commit
50b8f3ab94
5 changed files with 199 additions and 4 deletions
  1. 1 1
      Makefile
  2. 1 1
      cmd/sing-box/cmd_merge.go
  3. 162 0
      cmd/sing-box/cmd_rule_set_merge.go
  4. 4 2
      option/rule_set.go
  5. 31 0
      release/completions/sing-box.bash

+ 1 - 1
Makefile

@@ -28,7 +28,7 @@ ci_build:
 	go build $(MAIN_PARAMS) $(MAIN)
 
 generate_completions:
-	go run -v --tags generate,generate_completions $(MAIN)
+	go run -v --tags $(TAGS),generate,generate_completions $(MAIN)
 
 install:
 	go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)

+ 1 - 1
cmd/sing-box/cmd_merge.go

@@ -18,7 +18,7 @@ import (
 )
 
 var commandMerge = &cobra.Command{
-	Use:   "merge <output>",
+	Use:   "merge <output-path>",
 	Short: "Merge configurations",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := merge(args[0])

+ 162 - 0
cmd/sing-box/cmd_rule_set_merge.go

@@ -0,0 +1,162 @@
+package main
+
+import (
+	"bytes"
+	"io"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
+	"github.com/sagernet/sing/common/rw"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	ruleSetPaths       []string
+	ruleSetDirectories []string
+)
+
+var commandRuleSetMerge = &cobra.Command{
+	Use:   "merge <output-path>",
+	Short: "Merge rule-set source files",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := mergeRuleSet(args[0])
+		if err != nil {
+			log.Fatal(err)
+		}
+	},
+	Args: cobra.ExactArgs(1),
+}
+
+func init() {
+	commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
+	commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
+	commandRuleSet.AddCommand(commandRuleSetMerge)
+}
+
+type RuleSetEntry struct {
+	content []byte
+	path    string
+	options option.PlainRuleSetCompat
+}
+
+func readRuleSetAt(path string) (*RuleSetEntry, error) {
+	var (
+		configContent []byte
+		err           error
+	)
+	if path == "stdin" {
+		configContent, err = io.ReadAll(os.Stdin)
+	} else {
+		configContent, err = os.ReadFile(path)
+	}
+	if err != nil {
+		return nil, E.Cause(err, "read config at ", path)
+	}
+	options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
+	if err != nil {
+		return nil, E.Cause(err, "decode config at ", path)
+	}
+	return &RuleSetEntry{
+		content: configContent,
+		path:    path,
+		options: options,
+	}, nil
+}
+
+func readRuleSet() ([]*RuleSetEntry, error) {
+	var optionsList []*RuleSetEntry
+	for _, path := range ruleSetPaths {
+		optionsEntry, err := readRuleSetAt(path)
+		if err != nil {
+			return nil, err
+		}
+		optionsList = append(optionsList, optionsEntry)
+	}
+	for _, directory := range ruleSetDirectories {
+		entries, err := os.ReadDir(directory)
+		if err != nil {
+			return nil, E.Cause(err, "read rule-set directory at ", directory)
+		}
+		for _, entry := range entries {
+			if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
+				continue
+			}
+			optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
+			if err != nil {
+				return nil, err
+			}
+			optionsList = append(optionsList, optionsEntry)
+		}
+	}
+	sort.Slice(optionsList, func(i, j int) bool {
+		return optionsList[i].path < optionsList[j].path
+	})
+	return optionsList, nil
+}
+
+func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
+	optionsList, err := readRuleSet()
+	if err != nil {
+		return option.PlainRuleSetCompat{}, err
+	}
+	if len(optionsList) == 1 {
+		return optionsList[0].options, nil
+	}
+	var optionVersion uint8
+	for _, options := range optionsList {
+		if optionVersion < options.options.Version {
+			optionVersion = options.options.Version
+		}
+	}
+	var mergedMessage json.RawMessage
+	for _, options := range optionsList {
+		mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
+		if err != nil {
+			return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
+		}
+	}
+	mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
+	if err != nil {
+		return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
+	}
+	mergedOptions.Version = optionVersion
+	return mergedOptions, nil
+}
+
+func mergeRuleSet(outputPath string) error {
+	mergedOptions, err := readRuleSetAndMerge()
+	if err != nil {
+		return err
+	}
+	buffer := new(bytes.Buffer)
+	encoder := json.NewEncoder(buffer)
+	encoder.SetIndent("", "  ")
+	err = encoder.Encode(mergedOptions)
+	if err != nil {
+		return E.Cause(err, "encode config")
+	}
+	if existsContent, err := os.ReadFile(outputPath); err != nil {
+		if string(existsContent) == buffer.String() {
+			return nil
+		}
+	}
+	err = rw.MkdirParent(outputPath)
+	if err != nil {
+		return err
+	}
+	err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
+	if err != nil {
+		return err
+	}
+	outputPath, _ = filepath.Abs(outputPath)
+	os.Stderr.WriteString(outputPath + "\n")
+	return nil
+}

+ 4 - 2
option/rule_set.go

@@ -194,8 +194,9 @@ func (r LogicalHeadlessRule) IsValid() bool {
 }
 
 type _PlainRuleSetCompat struct {
-	Version uint8        `json:"version"`
-	Options PlainRuleSet `json:"-"`
+	Version    uint8           `json:"version"`
+	Options    PlainRuleSet    `json:"-"`
+	RawMessage json.RawMessage `json:"-"`
 }
 
 type PlainRuleSetCompat _PlainRuleSetCompat
@@ -229,6 +230,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
 	if err != nil {
 		return err
 	}
+	r.RawMessage = bytes
 	return nil
 }
 

+ 31 - 0
release/completions/sing-box.bash

@@ -1179,6 +1179,36 @@ _sing-box_rule-set_match()
     noun_aliases=()
 }
 
+_sing-box_rule-set_merge()
+{
+    last_command="sing-box_rule-set_merge"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
 _sing-box_rule-set_upgrade()
 {
     last_command="sing-box_rule-set_upgrade"
@@ -1225,6 +1255,7 @@ _sing-box_rule-set()
     commands+=("decompile")
     commands+=("format")
     commands+=("match")
+    commands+=("merge")
     commands+=("upgrade")
 
     flags=()