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 = deepMerge(combined, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(combined["rules"]))
|
combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(dedupeGenericRule(combined["rules"])))
|
||||||
mergeProxySelection(combined, selectionGroups)
|
mergeProxySelection(combined, selectionGroups)
|
||||||
|
|
||||||
return combined, proxies, true
|
return combined, proxies, true
|
||||||
@ -421,14 +421,14 @@ func dedupeGeoIPRule(rulesRaw interface{}) interface{} {
|
|||||||
|
|
||||||
lastIndex := make(map[string]int)
|
lastIndex := make(map[string]int)
|
||||||
for i, item := range rules {
|
for i, item := range rules {
|
||||||
if key, ok := geoIPRuleKey(item); ok {
|
if ruleType, key, ok := ruleKey(item); ok && ruleType == "GEOIP" {
|
||||||
lastIndex[key] = i
|
lastIndex[key] = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered := make([]interface{}, 0, len(rules))
|
filtered := make([]interface{}, 0, len(rules))
|
||||||
for i, item := range 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
|
continue
|
||||||
}
|
}
|
||||||
filtered = append(filtered, item)
|
filtered = append(filtered, item)
|
||||||
@ -436,13 +436,50 @@ func dedupeGeoIPRule(rulesRaw interface{}) interface{} {
|
|||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
// geoIPRuleKey returns the "GEOIP,<payload>" portion of a rule line (i.e.
|
// dedupeGenericRule keeps only the first occurrence of each
|
||||||
// everything but its target and any trailing "no-resolve" modifier), and
|
// "TYPE,PAYLOAD" rule (e.g. "DOMAIN,example.com,...",
|
||||||
// whether the line is a GEOIP rule at all.
|
// "DOMAIN-SUFFIX,google.com,...", "IP-CIDR,10.0.0.0/8,...") contributed by
|
||||||
func geoIPRuleKey(item interface{}) (string, bool) {
|
// 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)
|
line, ok := item.(string)
|
||||||
if !ok || !strings.HasPrefix(line, "GEOIP,") {
|
if !ok {
|
||||||
return "", false
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
typeEnd := strings.Index(line, ",")
|
||||||
|
if typeEnd == -1 {
|
||||||
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
body := line
|
body := line
|
||||||
@ -452,9 +489,10 @@ func geoIPRuleKey(item interface{}) (string, bool) {
|
|||||||
|
|
||||||
idx := strings.LastIndex(body, ",")
|
idx := strings.LastIndex(body, ",")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return line, true
|
return "", "", false
|
||||||
}
|
}
|
||||||
return body[:idx], true
|
|
||||||
|
return line[:typeEnd], body[:idx], true
|
||||||
}
|
}
|
||||||
|
|
||||||
func openInEditor(path string) bool {
|
func openInEditor(path string) bool {
|
||||||
|
|||||||
@ -58,3 +58,36 @@ func TestDedupeGeoIPRuleNonList(t *testing.T) {
|
|||||||
t.Fatalf("expected nil passthrough, got %#v", got)
|
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