Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package ruleio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/prism/proxy/internal/store"
|
||||
)
|
||||
|
||||
const ExportVersion = 1
|
||||
|
||||
type ExportDoc struct {
|
||||
Version int `json:"version"`
|
||||
Rules []ExportRule `json:"rules"`
|
||||
}
|
||||
|
||||
type ExportRule struct {
|
||||
Priority int `json:"priority"`
|
||||
RuleType string `json:"rule_type"`
|
||||
Payload string `json:"payload"`
|
||||
Target string `json:"target"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
type ImportResult struct {
|
||||
Imported int `json:"imported"`
|
||||
Skipped int `json:"skipped"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func ruleEnabled(r ExportRule) bool {
|
||||
if r.Enabled == nil {
|
||||
return true
|
||||
}
|
||||
return *r.Enabled
|
||||
}
|
||||
|
||||
func ToExportRules(rules []store.Rule) []ExportRule {
|
||||
out := make([]ExportRule, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
enabled := r.Enabled
|
||||
out = append(out, ExportRule{
|
||||
Priority: r.Priority,
|
||||
RuleType: r.RuleType,
|
||||
Payload: r.Payload,
|
||||
Target: r.Target,
|
||||
Enabled: &enabled,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ExportJSON(rules []store.Rule) ([]byte, error) {
|
||||
doc := ExportDoc{Version: ExportVersion, Rules: ToExportRules(rules)}
|
||||
return json.MarshalIndent(doc, "", " ")
|
||||
}
|
||||
|
||||
func ExportText(rules []store.Rule) string {
|
||||
var b strings.Builder
|
||||
for _, r := range rules {
|
||||
if !r.Enabled {
|
||||
continue
|
||||
}
|
||||
b.WriteString(formatRuleLine(r))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatRuleLine(r store.Rule) string {
|
||||
if r.Payload == "" {
|
||||
return fmt.Sprintf("%s,%s", r.RuleType, r.Target)
|
||||
}
|
||||
return fmt.Sprintf("%s,%s,%s", r.RuleType, r.Payload, r.Target)
|
||||
}
|
||||
|
||||
func ParseJSON(data []byte) ([]ExportRule, error) {
|
||||
var doc ExportDoc
|
||||
if err := json.Unmarshal(data, &doc); err == nil && (len(doc.Rules) > 0 || doc.Version > 0) {
|
||||
return doc.Rules, nil
|
||||
}
|
||||
var rules []ExportRule
|
||||
if err := json.Unmarshal(data, &rules); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func ParseText(text string) ([]ExportRule, []string) {
|
||||
lines := strings.Split(text, "\n")
|
||||
var out []ExportRule
|
||||
var errs []string
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
r, err := ParseLine(line)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("第 %d 行: %v", i+1, err))
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, errs
|
||||
}
|
||||
|
||||
func ParseLine(line string) (ExportRule, error) {
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) < 2 {
|
||||
return ExportRule{}, fmt.Errorf("格式无效 %q", line)
|
||||
}
|
||||
ruleType := strings.TrimSpace(strings.ToUpper(parts[0]))
|
||||
if ruleType == "MATCH" {
|
||||
return ExportRule{}, fmt.Errorf("跳过 MATCH 规则")
|
||||
}
|
||||
target := strings.TrimSpace(parts[len(parts)-1])
|
||||
if target == "" {
|
||||
return ExportRule{}, fmt.Errorf("缺少出站目标")
|
||||
}
|
||||
payload := ""
|
||||
if len(parts) > 2 {
|
||||
payload = strings.Join(parts[1:len(parts)-1], ",")
|
||||
}
|
||||
if needsPayload(ruleType) && payload == "" {
|
||||
return ExportRule{}, fmt.Errorf("%s 需要匹配值", ruleType)
|
||||
}
|
||||
enabled := true
|
||||
return ExportRule{
|
||||
RuleType: ruleType,
|
||||
Payload: payload,
|
||||
Target: target,
|
||||
Enabled: &enabled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func needsPayload(ruleType string) bool {
|
||||
switch ruleType {
|
||||
case "DIRECT", "REJECT", "REJECT-DROP", "MATCH":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func ToStoreRules(rules []ExportRule) ([]store.Rule, []string) {
|
||||
out := make([]store.Rule, 0, len(rules))
|
||||
var errs []string
|
||||
for i, r := range rules {
|
||||
ruleType := strings.TrimSpace(strings.ToUpper(r.RuleType))
|
||||
target := strings.TrimSpace(r.Target)
|
||||
if ruleType == "" || ruleType == "MATCH" {
|
||||
errs = append(errs, fmt.Sprintf("规则 %d: 类型无效", i+1))
|
||||
continue
|
||||
}
|
||||
if target == "" {
|
||||
errs = append(errs, fmt.Sprintf("规则 %d: 缺少出站目标", i+1))
|
||||
continue
|
||||
}
|
||||
payload := strings.TrimSpace(r.Payload)
|
||||
if needsPayload(ruleType) && payload == "" {
|
||||
errs = append(errs, fmt.Sprintf("规则 %d: %s 需要匹配值", i+1, ruleType))
|
||||
continue
|
||||
}
|
||||
out = append(out, store.Rule{
|
||||
Priority: r.Priority,
|
||||
RuleType: ruleType,
|
||||
Payload: payload,
|
||||
Target: target,
|
||||
Source: "manual",
|
||||
Enabled: ruleEnabled(r),
|
||||
})
|
||||
}
|
||||
return out, errs
|
||||
}
|
||||
|
||||
func AssignPriorities(rules []store.Rule, start int) {
|
||||
if start <= 0 {
|
||||
start = 100
|
||||
}
|
||||
step := 10
|
||||
for i := range rules {
|
||||
if rules[i].Priority <= 0 {
|
||||
rules[i].Priority = start
|
||||
start += step
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package ruleio
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prism/proxy/internal/store"
|
||||
)
|
||||
|
||||
func TestParseLine(t *testing.T) {
|
||||
r, err := ParseLine("DOMAIN-SUFFIX,google.com,country:HK")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.RuleType != "DOMAIN-SUFFIX" || r.Payload != "google.com" || r.Target != "country:HK" {
|
||||
t.Fatalf("unexpected %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLineSkipsMatch(t *testing.T) {
|
||||
_, err := ParseLine("MATCH,DIRECT")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseText(t *testing.T) {
|
||||
rules, errs := ParseText("# comment\nDOMAIN,google.com,DIRECT\n\nDOMAIN-SUFFIX,foo.com,PROXY")
|
||||
if len(errs) != 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
if len(rules) != 2 {
|
||||
t.Fatalf("got %d rules", len(rules))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONDoc(t *testing.T) {
|
||||
rules, err := ParseJSON([]byte(`{"version":1,"rules":[{"rule_type":"DOMAIN","payload":"a.com","target":"DIRECT"}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rules) != 1 || rules[0].Target != "DIRECT" {
|
||||
t.Fatalf("unexpected %+v", rules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportText(t *testing.T) {
|
||||
text := ExportText([]store.Rule{{
|
||||
RuleType: "DOMAIN-SUFFIX",
|
||||
Payload: "x.com",
|
||||
Target: "PROXY",
|
||||
Enabled: true,
|
||||
}})
|
||||
if !strings.Contains(text, "DOMAIN-SUFFIX,x.com,PROXY") {
|
||||
t.Fatalf("got %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToStoreRulesDefaultsEnabled(t *testing.T) {
|
||||
rules, errs := ToStoreRules([]ExportRule{{RuleType: "DOMAIN", Payload: "a.com", Target: "DIRECT"}})
|
||||
if len(errs) != 0 || len(rules) != 1 || !rules[0].Enabled {
|
||||
t.Fatalf("rules=%+v errs=%v", rules, errs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user