Skip to content

Commit 71e57fa

Browse files
committed
Add rule-set merge command
1 parent e5bf032 commit 71e57fa

File tree

5 files changed

+199
-4
lines changed

5 files changed

+199
-4
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ ci_build:
2828
go build $(MAIN_PARAMS) $(MAIN)
2929

3030
generate_completions:
31-
go run -v --tags generate,generate_completions $(MAIN)
31+
go run -v --tags $(TAGS),generate,generate_completions $(MAIN)
3232

3333
install:
3434
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)

cmd/sing-box/cmd_merge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
)
1919

2020
var commandMerge = &cobra.Command{
21-
Use: "merge <output>",
21+
Use: "merge <output-path>",
2222
Short: "Merge configurations",
2323
Run: func(cmd *cobra.Command, args []string) {
2424
err := merge(args[0])

cmd/sing-box/cmd_rule_set_merge.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
"strings"
10+
11+
"github.com/sagernet/sing-box/log"
12+
"github.com/sagernet/sing-box/option"
13+
E "github.com/sagernet/sing/common/exceptions"
14+
"github.com/sagernet/sing/common/json"
15+
"github.com/sagernet/sing/common/json/badjson"
16+
"github.com/sagernet/sing/common/rw"
17+
18+
"github.com/spf13/cobra"
19+
)
20+
21+
var (
22+
ruleSetPaths []string
23+
ruleSetDirectories []string
24+
)
25+
26+
var commandRuleSetMerge = &cobra.Command{
27+
Use: "merge <output-path>",
28+
Short: "Merge rule-set source files",
29+
Run: func(cmd *cobra.Command, args []string) {
30+
err := mergeRuleSet(args[0])
31+
if err != nil {
32+
log.Fatal(err)
33+
}
34+
},
35+
Args: cobra.ExactArgs(1),
36+
}
37+
38+
func init() {
39+
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
40+
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
41+
commandRuleSet.AddCommand(commandRuleSetMerge)
42+
}
43+
44+
type RuleSetEntry struct {
45+
content []byte
46+
path string
47+
options option.PlainRuleSetCompat
48+
}
49+
50+
func readRuleSetAt(path string) (*RuleSetEntry, error) {
51+
var (
52+
configContent []byte
53+
err error
54+
)
55+
if path == "stdin" {
56+
configContent, err = io.ReadAll(os.Stdin)
57+
} else {
58+
configContent, err = os.ReadFile(path)
59+
}
60+
if err != nil {
61+
return nil, E.Cause(err, "read config at ", path)
62+
}
63+
options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
64+
if err != nil {
65+
return nil, E.Cause(err, "decode config at ", path)
66+
}
67+
return &RuleSetEntry{
68+
content: configContent,
69+
path: path,
70+
options: options,
71+
}, nil
72+
}
73+
74+
func readRuleSet() ([]*RuleSetEntry, error) {
75+
var optionsList []*RuleSetEntry
76+
for _, path := range ruleSetPaths {
77+
optionsEntry, err := readRuleSetAt(path)
78+
if err != nil {
79+
return nil, err
80+
}
81+
optionsList = append(optionsList, optionsEntry)
82+
}
83+
for _, directory := range ruleSetDirectories {
84+
entries, err := os.ReadDir(directory)
85+
if err != nil {
86+
return nil, E.Cause(err, "read rule-set directory at ", directory)
87+
}
88+
for _, entry := range entries {
89+
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
90+
continue
91+
}
92+
optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
93+
if err != nil {
94+
return nil, err
95+
}
96+
optionsList = append(optionsList, optionsEntry)
97+
}
98+
}
99+
sort.Slice(optionsList, func(i, j int) bool {
100+
return optionsList[i].path < optionsList[j].path
101+
})
102+
return optionsList, nil
103+
}
104+
105+
func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
106+
optionsList, err := readRuleSet()
107+
if err != nil {
108+
return option.PlainRuleSetCompat{}, err
109+
}
110+
if len(optionsList) == 1 {
111+
return optionsList[0].options, nil
112+
}
113+
var optionVersion uint8
114+
for _, options := range optionsList {
115+
if optionVersion < options.options.Version {
116+
optionVersion = options.options.Version
117+
}
118+
}
119+
var mergedMessage json.RawMessage
120+
for _, options := range optionsList {
121+
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
122+
if err != nil {
123+
return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
124+
}
125+
}
126+
mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
127+
if err != nil {
128+
return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
129+
}
130+
mergedOptions.Version = optionVersion
131+
return mergedOptions, nil
132+
}
133+
134+
func mergeRuleSet(outputPath string) error {
135+
mergedOptions, err := readRuleSetAndMerge()
136+
if err != nil {
137+
return err
138+
}
139+
buffer := new(bytes.Buffer)
140+
encoder := json.NewEncoder(buffer)
141+
encoder.SetIndent("", " ")
142+
err = encoder.Encode(mergedOptions)
143+
if err != nil {
144+
return E.Cause(err, "encode config")
145+
}
146+
if existsContent, err := os.ReadFile(outputPath); err != nil {
147+
if string(existsContent) == buffer.String() {
148+
return nil
149+
}
150+
}
151+
err = rw.MkdirParent(outputPath)
152+
if err != nil {
153+
return err
154+
}
155+
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
156+
if err != nil {
157+
return err
158+
}
159+
outputPath, _ = filepath.Abs(outputPath)
160+
os.Stderr.WriteString(outputPath + "\n")
161+
return nil
162+
}

option/rule_set.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,9 @@ func (r LogicalHeadlessRule) IsValid() bool {
194194
}
195195

196196
type _PlainRuleSetCompat struct {
197-
Version uint8 `json:"version"`
198-
Options PlainRuleSet `json:"-"`
197+
Version uint8 `json:"version"`
198+
Options PlainRuleSet `json:"-"`
199+
RawMessage json.RawMessage `json:"-"`
199200
}
200201

201202
type PlainRuleSetCompat _PlainRuleSetCompat
@@ -229,6 +230,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
229230
if err != nil {
230231
return err
231232
}
233+
r.RawMessage = bytes
232234
return nil
233235
}
234236

release/completions/sing-box.bash

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,36 @@ _sing-box_rule-set_match()
11791179
noun_aliases=()
11801180
}
11811181

1182+
_sing-box_rule-set_merge()
1183+
{
1184+
last_command="sing-box_rule-set_merge"
1185+
1186+
command_aliases=()
1187+
1188+
commands=()
1189+
1190+
flags=()
1191+
two_word_flags=()
1192+
local_nonpersistent_flags=()
1193+
flags_with_completion=()
1194+
flags_completion=()
1195+
1196+
flags+=("--config=")
1197+
two_word_flags+=("--config")
1198+
two_word_flags+=("-c")
1199+
flags+=("--config-directory=")
1200+
two_word_flags+=("--config-directory")
1201+
two_word_flags+=("-C")
1202+
flags+=("--directory=")
1203+
two_word_flags+=("--directory")
1204+
two_word_flags+=("-D")
1205+
flags+=("--disable-color")
1206+
1207+
must_have_one_flag=()
1208+
must_have_one_noun=()
1209+
noun_aliases=()
1210+
}
1211+
11821212
_sing-box_rule-set_upgrade()
11831213
{
11841214
last_command="sing-box_rule-set_upgrade"
@@ -1225,6 +1255,7 @@ _sing-box_rule-set()
12251255
commands+=("decompile")
12261256
commands+=("format")
12271257
commands+=("match")
1258+
commands+=("merge")
12281259
commands+=("upgrade")
12291260

12301261
flags=()

0 commit comments

Comments
 (0)