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 }