feat: support merging multiple subscriptions
This commit is contained in:
151
internal/automation/prefix.go
Normal file
151
internal/automation/prefix.go
Normal file
@ -0,0 +1,151 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PrefixProxyNames rewrites every proxy's "name" field in data["proxies"]
|
||||
// and every proxy-group's "name" field in data["proxy-groups"] — both are
|
||||
// scoped per-subscription and would otherwise collide across subscriptions
|
||||
// the same way proxy names do — prefixing them with subscriptionName. It
|
||||
// also rewrites every reference to a renamed proxy or proxy-group inside
|
||||
// each group's own "proxies" list and inside data["rules"]' rule targets
|
||||
// (e.g. "DOMAIN,example.com,Proxy" or "MATCH,Auto"), since those are all
|
||||
// resolved by name and would otherwise silently point at nothing after the
|
||||
// rename. References to anything else (DIRECT, REJECT, a proxy-provider
|
||||
// name, ...) are left untouched.
|
||||
//
|
||||
// Returns a ProxyRef per proxy, for later ssm matching/grouping.
|
||||
func PrefixProxyNames(subscriptionName string, data map[string]interface{}) []ProxyRef {
|
||||
// Original name (of either a proxy or a proxy-group) -> prefixed
|
||||
// display name. Clash requires proxy and group names to share a single
|
||||
// namespace within one config, so a single map is correct here too.
|
||||
rename := make(map[string]string)
|
||||
|
||||
refs := renameProxies(subscriptionName, data, rename)
|
||||
renameProxyGroups(subscriptionName, data, rename)
|
||||
rewriteGroupProxyReferences(data, rename)
|
||||
rewriteRuleTargets(data, rename)
|
||||
|
||||
return refs
|
||||
}
|
||||
|
||||
func renameProxies(subscriptionName string, data map[string]interface{}, rename map[string]string) []ProxyRef {
|
||||
proxiesRaw, ok := data["proxies"].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
refs := make([]ProxyRef, 0, len(proxiesRaw))
|
||||
for _, item := range proxiesRaw {
|
||||
proxy, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, ok := proxy["name"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
display := fmt.Sprintf("%s | %s", subscriptionName, name)
|
||||
proxy["name"] = display
|
||||
rename[name] = display
|
||||
|
||||
refs = append(refs, ProxyRef{
|
||||
OriginalName: name,
|
||||
DisplayName: display,
|
||||
Subscription: subscriptionName,
|
||||
})
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func renameProxyGroups(subscriptionName string, data map[string]interface{}, rename map[string]string) {
|
||||
for _, group := range proxyGroupMaps(data) {
|
||||
name, ok := group["name"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
display := fmt.Sprintf("%s | %s", subscriptionName, name)
|
||||
group["name"] = display
|
||||
rename[name] = display
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteGroupProxyReferences(data map[string]interface{}, rename map[string]string) {
|
||||
for _, group := range proxyGroupMaps(data) {
|
||||
proxiesRaw, ok := group["proxies"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i, p := range proxiesRaw {
|
||||
name, ok := p.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if display, renamed := rename[name]; renamed {
|
||||
proxiesRaw[i] = display
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rewriteRuleTargets rewrites the target field of every rule in
|
||||
// data["rules"] that references a renamed proxy or proxy-group. Clash rule
|
||||
// lines are a comma-separated "TYPE,payload,TARGET[,no-resolve]" (or just
|
||||
// "MATCH,TARGET" with no payload) — the target is always the last field,
|
||||
// except for the handful of rule types that allow a trailing "no-resolve"
|
||||
// modifier, where it's the second-to-last. Lines with no comma at all are
|
||||
// left untouched (not valid rule syntax to begin with).
|
||||
func rewriteRuleTargets(data map[string]interface{}, rename map[string]string) {
|
||||
rulesRaw, ok := data["rules"].([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for i, item := range rulesRaw {
|
||||
line, ok := item.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rulesRaw[i] = rewriteRuleLine(line, rename)
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteRuleLine(line string, rename map[string]string) string {
|
||||
body := line
|
||||
suffix := ""
|
||||
|
||||
if idx := strings.LastIndex(body, ","); idx != -1 && strings.EqualFold(body[idx+1:], "no-resolve") {
|
||||
suffix = "," + body[idx+1:]
|
||||
body = body[:idx]
|
||||
}
|
||||
|
||||
idx := strings.LastIndex(body, ",")
|
||||
if idx == -1 {
|
||||
return line
|
||||
}
|
||||
|
||||
target := body[idx+1:]
|
||||
if display, ok := rename[target]; ok {
|
||||
body = body[:idx+1] + display
|
||||
}
|
||||
|
||||
return body + suffix
|
||||
}
|
||||
|
||||
// proxyGroupMaps returns data["proxy-groups"]'s entries as maps, skipping
|
||||
// anything malformed.
|
||||
func proxyGroupMaps(data map[string]interface{}) []map[string]interface{} {
|
||||
groupsRaw, ok := data["proxy-groups"].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
groups := make([]map[string]interface{}, 0, len(groupsRaw))
|
||||
for _, item := range groupsRaw {
|
||||
if group, ok := item.(map[string]interface{}); ok {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
Reference in New Issue
Block a user