feat: dedupe generic rules
This commit is contained in:
@ -381,7 +381,7 @@ func (m *Manager) mergeSubscriptions(subs []*model.Subscription) (map[string]int
|
||||
combined = deepMerge(combined, data)
|
||||
}
|
||||
|
||||
combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(combined["rules"]))
|
||||
combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(dedupeGenericRule(combined["rules"])))
|
||||
mergeProxySelection(combined, selectionGroups)
|
||||
|
||||
return combined, proxies, true
|
||||
@ -421,14 +421,14 @@ func dedupeGeoIPRule(rulesRaw interface{}) interface{} {
|
||||
|
||||
lastIndex := make(map[string]int)
|
||||
for i, item := range rules {
|
||||
if key, ok := geoIPRuleKey(item); ok {
|
||||
if ruleType, key, ok := ruleKey(item); ok && ruleType == "GEOIP" {
|
||||
lastIndex[key] = i
|
||||
}
|
||||
}
|
||||
|
||||
filtered := make([]interface{}, 0, len(rules))
|
||||
for i, item := range rules {
|
||||
if key, ok := geoIPRuleKey(item); ok && lastIndex[key] != i {
|
||||
if ruleType, key, ok := ruleKey(item); ok && ruleType == "GEOIP" && lastIndex[key] != i {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, item)
|
||||
@ -436,13 +436,50 @@ func dedupeGeoIPRule(rulesRaw interface{}) interface{} {
|
||||
return filtered
|
||||
}
|
||||
|
||||
// geoIPRuleKey returns the "GEOIP,<payload>" portion of a rule line (i.e.
|
||||
// everything but its target and any trailing "no-resolve" modifier), and
|
||||
// whether the line is a GEOIP rule at all.
|
||||
func geoIPRuleKey(item interface{}) (string, bool) {
|
||||
// dedupeGenericRule keeps only the first occurrence of each
|
||||
// "TYPE,PAYLOAD" rule (e.g. "DOMAIN,example.com,...",
|
||||
// "DOMAIN-SUFFIX,google.com,...", "IP-CIDR,10.0.0.0/8,...") contributed by
|
||||
// the merged subscriptions, dropping the rest: Clash evaluates rules
|
||||
// top-to-bottom, so once an earlier rule matches a given payload, any later
|
||||
// duplicate for that same payload could never fire anyway. MATCH and GEOIP
|
||||
// rules have their own dedup semantics (dedupeMatchRule, dedupeGeoIPRule),
|
||||
// so they pass through here untouched.
|
||||
func dedupeGenericRule(rulesRaw interface{}) interface{} {
|
||||
rules, ok := rulesRaw.([]interface{})
|
||||
if !ok {
|
||||
return rulesRaw
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
filtered := make([]interface{}, 0, len(rules))
|
||||
for _, item := range rules {
|
||||
ruleType, key, ok := ruleKey(item)
|
||||
if !ok || ruleType == "MATCH" || ruleType == "GEOIP" {
|
||||
filtered = append(filtered, item)
|
||||
continue
|
||||
}
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// ruleKey returns a rule line's type (e.g. "DOMAIN", "GEOIP", "MATCH") and
|
||||
// its "TYPE,PAYLOAD" portion — everything but the target and any trailing
|
||||
// "no-resolve" modifier — plus whether the line parses as a well-formed
|
||||
// "TYPE,PAYLOAD,TARGET" (or "TYPE,TARGET" for MATCH) rule at all.
|
||||
func ruleKey(item interface{}) (ruleType, key string, ok bool) {
|
||||
line, ok := item.(string)
|
||||
if !ok || !strings.HasPrefix(line, "GEOIP,") {
|
||||
return "", false
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
typeEnd := strings.Index(line, ",")
|
||||
if typeEnd == -1 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
body := line
|
||||
@ -452,9 +489,10 @@ func geoIPRuleKey(item interface{}) (string, bool) {
|
||||
|
||||
idx := strings.LastIndex(body, ",")
|
||||
if idx == -1 {
|
||||
return line, true
|
||||
return "", "", false
|
||||
}
|
||||
return body[:idx], true
|
||||
|
||||
return line[:typeEnd], body[:idx], true
|
||||
}
|
||||
|
||||
func openInEditor(path string) bool {
|
||||
|
||||
@ -58,3 +58,36 @@ func TestDedupeGeoIPRuleNonList(t *testing.T) {
|
||||
t.Fatalf("expected nil passthrough, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupeGenericRule(t *testing.T) {
|
||||
rules := []interface{}{
|
||||
"DOMAIN,example.com,sub-a | Proxy",
|
||||
"DOMAIN-SUFFIX,google.com,sub-a | Proxy",
|
||||
"DOMAIN,example.com,sub-b | Proxy",
|
||||
"IP-CIDR,10.0.0.0/8,DIRECT,no-resolve",
|
||||
"DOMAIN-SUFFIX,google.com,sub-b | Proxy",
|
||||
"IP-CIDR,10.0.0.0/8,sub-b | Proxy",
|
||||
"GEOIP,CN,DIRECT",
|
||||
"MATCH,sub-a | Auto",
|
||||
}
|
||||
|
||||
got := dedupeGenericRule(rules)
|
||||
|
||||
want := []interface{}{
|
||||
"DOMAIN,example.com,sub-a | Proxy",
|
||||
"DOMAIN-SUFFIX,google.com,sub-a | Proxy",
|
||||
"IP-CIDR,10.0.0.0/8,DIRECT,no-resolve",
|
||||
"GEOIP,CN,DIRECT",
|
||||
"MATCH,sub-a | Auto",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupeGenericRuleNonList(t *testing.T) {
|
||||
if got := dedupeGenericRule(nil); got != nil {
|
||||
t.Fatalf("expected nil passthrough, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user