first commit
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
This commit is contained in:
653
internal/generator/clash.go
Normal file
653
internal/generator/clash.go
Normal file
@@ -0,0 +1,653 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// ClashGenerator Clash格式生成器
|
||||
// 实现Clash代理配置的生成功能
|
||||
type ClashGenerator struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewClashGenerator 创建新的Clash生成器
|
||||
// 返回初始化好的ClashGenerator实例
|
||||
func NewClashGenerator(logger *logging.Logger) *ClashGenerator {
|
||||
return &ClashGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成Clash配置
|
||||
func (g *ClashGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
|
||||
g.logger.Debugf("Generating Clash configuration")
|
||||
|
||||
// 创建Clash配置结构
|
||||
clashConfig := g.createClashConfig(configs, options)
|
||||
|
||||
// 转换为YAML
|
||||
yamlData, err := yaml.Marshal(clashConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal Clash configuration: %v", err)
|
||||
}
|
||||
|
||||
return string(yamlData), nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证Clash生成选项
|
||||
func (g *ClashGenerator) ValidateOptions(options *GenerationOptions) error {
|
||||
g.logger.Debugf("Validating Clash generation options")
|
||||
|
||||
// 验证基本信息
|
||||
if options.Name == "" {
|
||||
return fmt.Errorf("configuration name is required")
|
||||
}
|
||||
|
||||
// 验证模式
|
||||
validModes := []string{"global", "rule", "direct"}
|
||||
if options.Mode != "" {
|
||||
valid := false
|
||||
for _, mode := range validModes {
|
||||
if options.Mode == mode {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("invalid mode: %s, must be one of: %s", options.Mode, strings.Join(validModes, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
if options.MixedPort < 0 || options.MixedPort > 65535 {
|
||||
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
|
||||
}
|
||||
|
||||
// 验证日志级别
|
||||
validLogLevels := []string{"info", "warning", "error", "debug", "silent"}
|
||||
if options.LogLevel != "" {
|
||||
valid := false
|
||||
for _, level := range validLogLevels {
|
||||
if options.LogLevel == level {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("invalid log level: %s, must be one of: %s", options.LogLevel, strings.Join(validLogLevels, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
func (g *ClashGenerator) GetSupportedFormats() []string {
|
||||
return []string{"clash"}
|
||||
}
|
||||
|
||||
// ClashConfig Clash配置结构体
|
||||
type ClashConfig struct {
|
||||
Port int `yaml:"port" json:"port"`
|
||||
SocksPort int `yaml:"socks-port" json:"socks-port"`
|
||||
AllowLan bool `yaml:"allow-lan" json:"allow-lan"`
|
||||
Mode string `yaml:"mode" json:"mode"`
|
||||
LogLevel string `yaml:"log-level" json:"log-level"`
|
||||
ExternalController string `yaml:"external-controller" json:"external-controller"`
|
||||
Secret string `yaml:"secret,omitempty" json:"secret,omitempty"`
|
||||
Proxies []map[string]interface{} `yaml:"proxies" json:"proxies"`
|
||||
ProxyGroup []map[string]interface{} `yaml:"proxy-groups" json:"proxy-groups"`
|
||||
Rules []string `yaml:"rules" json:"rules"`
|
||||
ProxyProviders map[string]map[string]interface{} `yaml:"proxy-providers,omitempty" json:"proxy-providers,omitempty"`
|
||||
}
|
||||
|
||||
// createClashConfig 创建Clash配置结构
|
||||
func (g *ClashGenerator) createClashConfig(configs []*parser.ProxyConfig, options *GenerationOptions) *ClashConfig {
|
||||
config := &ClashConfig{
|
||||
Port: 7890,
|
||||
SocksPort: 7891,
|
||||
AllowLan: options.AllowLan,
|
||||
Mode: options.Mode,
|
||||
LogLevel: options.LogLevel,
|
||||
ExternalController: options.ExternalController,
|
||||
Secret: options.Secret,
|
||||
Proxies: make([]map[string]interface{}, 0),
|
||||
ProxyGroup: make([]map[string]interface{}, 0),
|
||||
Rules: make([]string, 0),
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if config.Port == 0 {
|
||||
config.Port = 7890
|
||||
}
|
||||
if config.SocksPort == 0 {
|
||||
config.SocksPort = 7891
|
||||
}
|
||||
if config.Mode == "" {
|
||||
config.Mode = "rule"
|
||||
}
|
||||
if config.LogLevel == "" {
|
||||
config.LogLevel = "info"
|
||||
}
|
||||
|
||||
// 生成代理配置
|
||||
for _, proxyConfig := range configs {
|
||||
proxy := g.convertProxyConfig(proxyConfig)
|
||||
config.Proxies = append(config.Proxies, proxy)
|
||||
}
|
||||
|
||||
// 生成代理组
|
||||
config.ProxyGroup = g.createProxyGroups(configs, options)
|
||||
if len(options.GroupDefinitions) > 0 {
|
||||
for _, def := range options.GroupDefinitions {
|
||||
config.ProxyGroup = append(config.ProxyGroup, g.buildClashGroup(def, configs))
|
||||
}
|
||||
} else if len(options.CustomGroups) > 0 {
|
||||
for _, entry := range options.CustomGroups {
|
||||
group, err := g.parseCustomGroup(entry, configs)
|
||||
if err != nil {
|
||||
g.logger.WithError(err).Warnf("Failed to parse custom group: %s", entry)
|
||||
continue
|
||||
}
|
||||
config.ProxyGroup = append(config.ProxyGroup, group)
|
||||
}
|
||||
}
|
||||
|
||||
if len(options.Providers) > 0 {
|
||||
providers := make(map[string]map[string]interface{}, len(options.Providers))
|
||||
for _, def := range options.Providers {
|
||||
providers[def.Name] = g.buildClashProvider(def)
|
||||
}
|
||||
config.ProxyProviders = providers
|
||||
} else if len(options.CustomProviders) > 0 {
|
||||
providers := make(map[string]map[string]interface{})
|
||||
for _, entry := range options.CustomProviders {
|
||||
parsed, err := ParseProviderDefinition(entry)
|
||||
if err != nil {
|
||||
g.logger.WithError(err).Warnf("Failed to parse custom provider: %s", entry)
|
||||
continue
|
||||
}
|
||||
providers[parsed.Name] = g.buildClashProvider(parsed)
|
||||
}
|
||||
if len(providers) > 0 {
|
||||
config.ProxyProviders = providers
|
||||
}
|
||||
}
|
||||
|
||||
// 生成规则
|
||||
rules := g.createRules(options)
|
||||
config.Rules = g.applyCustomRulesets(rules, options)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// convertProxyConfig 转换代理配置为Clash格式
|
||||
func (g *ClashGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) map[string]interface{} {
|
||||
proxy := map[string]interface{}{
|
||||
"name": proxyConfig.Name,
|
||||
"type": proxyConfig.Type,
|
||||
"server": proxyConfig.Server,
|
||||
"port": proxyConfig.Port,
|
||||
"udp": proxyConfig.UDP,
|
||||
}
|
||||
|
||||
// 根据协议类型添加特定配置
|
||||
switch proxyConfig.Protocol {
|
||||
case "ss":
|
||||
if method, exists := proxyConfig.Settings["method"]; exists {
|
||||
proxy["cipher"] = method
|
||||
}
|
||||
if password, exists := proxyConfig.Settings["password"]; exists {
|
||||
proxy["password"] = password
|
||||
}
|
||||
if plugin, exists := proxyConfig.Settings["plugin"]; exists {
|
||||
proxy["plugin"] = plugin
|
||||
}
|
||||
|
||||
case "ssr":
|
||||
if method, exists := proxyConfig.Settings["method"]; exists {
|
||||
proxy["cipher"] = method
|
||||
}
|
||||
if password, exists := proxyConfig.Settings["password"]; exists {
|
||||
proxy["password"] = password
|
||||
}
|
||||
if protocol, exists := proxyConfig.Settings["protocol"]; exists {
|
||||
proxy["protocol"] = protocol
|
||||
}
|
||||
if obfs, exists := proxyConfig.Settings["obfs"]; exists {
|
||||
proxy["obfs"] = obfs
|
||||
}
|
||||
|
||||
case "vmess":
|
||||
if uuid, exists := proxyConfig.Settings["uuid"]; exists {
|
||||
proxy["uuid"] = uuid
|
||||
}
|
||||
if alterId, exists := proxyConfig.Settings["alterId"]; exists {
|
||||
proxy["alterId"] = alterId
|
||||
}
|
||||
if network, exists := proxyConfig.Settings["network"]; exists {
|
||||
proxy["network"] = network
|
||||
}
|
||||
if tls, exists := proxyConfig.Settings["tls"]; exists {
|
||||
proxy["tls"] = tls
|
||||
}
|
||||
if host, exists := proxyConfig.Settings["host"]; exists {
|
||||
proxy["servername"] = host
|
||||
}
|
||||
if path, exists := proxyConfig.Settings["path"]; exists {
|
||||
proxy["ws-path"] = path
|
||||
}
|
||||
|
||||
case "trojan":
|
||||
if password, exists := proxyConfig.Settings["password"]; exists {
|
||||
proxy["password"] = password
|
||||
}
|
||||
if sni, exists := proxyConfig.Settings["sni"]; exists {
|
||||
proxy["sni"] = sni
|
||||
}
|
||||
if network, exists := proxyConfig.Settings["network"]; exists {
|
||||
proxy["network"] = network
|
||||
}
|
||||
|
||||
case "http", "https":
|
||||
if username, exists := proxyConfig.Settings["username"]; exists {
|
||||
proxy["username"] = username
|
||||
}
|
||||
if password, exists := proxyConfig.Settings["password"]; exists {
|
||||
proxy["password"] = password
|
||||
}
|
||||
if tls, exists := proxyConfig.Settings["tls"]; exists {
|
||||
proxy["tls"] = tls
|
||||
}
|
||||
|
||||
case "socks5":
|
||||
if username, exists := proxyConfig.Settings["username"]; exists {
|
||||
proxy["username"] = username
|
||||
}
|
||||
if password, exists := proxyConfig.Settings["password"]; exists {
|
||||
proxy["password"] = password
|
||||
}
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
// createProxyGroups 创建代理组
|
||||
func (g *ClashGenerator) createProxyGroups(configs []*parser.ProxyConfig, options *GenerationOptions) []map[string]interface{} {
|
||||
groups := make([]map[string]interface{}, 0)
|
||||
|
||||
// 创建代理名称列表
|
||||
proxyNames := make([]string, 0)
|
||||
for _, config := range configs {
|
||||
proxyNames = append(proxyNames, config.Name)
|
||||
}
|
||||
|
||||
// 创建选择代理组
|
||||
selectGroup := map[string]interface{}{
|
||||
"name": "PROXY",
|
||||
"type": "select",
|
||||
"proxies": proxyNames,
|
||||
}
|
||||
groups = append(groups, selectGroup)
|
||||
|
||||
// 创建自动选择代理组(如果启用了代理测试)
|
||||
if options.ProxyTest {
|
||||
urlTestGroup := map[string]interface{}{
|
||||
"name": "URL-TEST",
|
||||
"type": "url-test",
|
||||
"proxies": proxyNames,
|
||||
"url": options.ProxyURL,
|
||||
"interval": 300,
|
||||
"tolerance": 50,
|
||||
}
|
||||
if urlTestGroup["url"] == "" {
|
||||
urlTestGroup["url"] = "http://www.gstatic.com/generate_204"
|
||||
}
|
||||
groups = append(groups, urlTestGroup)
|
||||
}
|
||||
|
||||
// 创建故障转移代理组
|
||||
fallbackGroup := map[string]interface{}{
|
||||
"name": "FALLBACK",
|
||||
"type": "fallback",
|
||||
"proxies": proxyNames,
|
||||
"url": "http://www.gstatic.com/generate_204",
|
||||
"interval": 300,
|
||||
}
|
||||
groups = append(groups, fallbackGroup)
|
||||
|
||||
// 创建直连和拒绝代理组
|
||||
groups = append(groups, map[string]interface{}{
|
||||
"name": "DIRECT",
|
||||
"type": "select",
|
||||
"proxies": []string{"DIRECT"},
|
||||
})
|
||||
|
||||
groups = append(groups, map[string]interface{}{
|
||||
"name": "REJECT",
|
||||
"type": "select",
|
||||
"proxies": []string{"REJECT"},
|
||||
})
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func (g *ClashGenerator) buildClashGroup(def *GroupDefinition, configs []*parser.ProxyConfig) map[string]interface{} {
|
||||
group := map[string]interface{}{
|
||||
"name": def.Name,
|
||||
"type": def.Type,
|
||||
}
|
||||
|
||||
proxies := make([]string, 0, len(def.Proxies))
|
||||
proxies = append(proxies, def.Proxies...)
|
||||
if len(proxies) == 0 && len(def.UseProviders) == 0 {
|
||||
for _, cfg := range configs {
|
||||
proxies = append(proxies, cfg.Name)
|
||||
}
|
||||
}
|
||||
if len(proxies) > 0 {
|
||||
group["proxies"] = proxies
|
||||
}
|
||||
if len(def.UseProviders) > 0 {
|
||||
group["use"] = def.UseProviders
|
||||
}
|
||||
if def.URL != "" {
|
||||
group["url"] = def.URL
|
||||
}
|
||||
if def.Interval != nil {
|
||||
group["interval"] = *def.Interval
|
||||
}
|
||||
if def.Timeout != nil {
|
||||
group["timeout"] = *def.Timeout
|
||||
}
|
||||
if def.Tolerance != nil {
|
||||
group["tolerance"] = *def.Tolerance
|
||||
}
|
||||
for k, v := range def.Extras {
|
||||
group[k] = v
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
func (g *ClashGenerator) buildClashProvider(def *ProviderDefinition) map[string]interface{} {
|
||||
provider := map[string]interface{}{
|
||||
"type": def.Type,
|
||||
"url": def.URL,
|
||||
"path": def.Path,
|
||||
}
|
||||
if def.Interval != nil {
|
||||
provider["interval"] = *def.Interval
|
||||
}
|
||||
if len(def.Fields) > 0 {
|
||||
keys := make([]string, 0, len(def.Fields))
|
||||
for k := range def.Fields {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
provider[k] = def.Fields[k]
|
||||
}
|
||||
}
|
||||
if len(def.Flags) > 0 {
|
||||
keys := make([]string, 0, len(def.Flags))
|
||||
for k := range def.Flags {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
provider[k] = def.Flags[k]
|
||||
}
|
||||
}
|
||||
if def.Health != nil {
|
||||
health := make(map[string]interface{})
|
||||
if def.Health.Enable != nil {
|
||||
health["enable"] = *def.Health.Enable
|
||||
}
|
||||
if def.Health.URL != "" {
|
||||
health["url"] = def.Health.URL
|
||||
}
|
||||
if def.Health.Interval != nil {
|
||||
health["interval"] = *def.Health.Interval
|
||||
}
|
||||
if def.Health.Lazy != nil {
|
||||
health["lazy"] = *def.Health.Lazy
|
||||
}
|
||||
if def.Health.Tolerance != nil {
|
||||
health["tolerance"] = *def.Health.Tolerance
|
||||
}
|
||||
if def.Health.Timeout != nil {
|
||||
health["timeout"] = *def.Health.Timeout
|
||||
}
|
||||
if def.Health.Method != "" {
|
||||
health["method"] = def.Health.Method
|
||||
}
|
||||
if def.Health.Headers != "" {
|
||||
health["headers"] = def.Health.Headers
|
||||
}
|
||||
if len(health) > 0 {
|
||||
provider["health-check"] = health
|
||||
}
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
// createRules 创建规则
|
||||
func (g *ClashGenerator) createRules(options *GenerationOptions) []string {
|
||||
rules := make([]string, 0)
|
||||
|
||||
// 添加默认规则
|
||||
if len(options.Rules) > 0 {
|
||||
rules = append(rules, options.Rules...)
|
||||
} else {
|
||||
// 添加默认规则集
|
||||
defaultRules := []string{
|
||||
"DOMAIN-SUFFIX,bilibili.com,DIRECT",
|
||||
"DOMAIN-SUFFIX,baidu.com,DIRECT",
|
||||
"DOMAIN-SUFFIX,cn,DIRECT",
|
||||
"DOMAIN-KEYWORD,google,PROXY",
|
||||
"DOMAIN-KEYWORD,github,PROXY",
|
||||
"DOMAIN-KEYWORD,twitter,PROXY",
|
||||
"DOMAIN-KEYWORD,facebook,PROXY",
|
||||
"DOMAIN-KEYWORD,youtube,PROXY",
|
||||
"DOMAIN-KEYWORD,instagram,PROXY",
|
||||
"DOMAIN-KEYWORD,telegram,PROXY",
|
||||
"GEOIP,CN,DIRECT",
|
||||
"IP-CIDR,127.0.0.0/8,DIRECT",
|
||||
"IP-CIDR,192.168.0.0/16,DIRECT",
|
||||
"IP-CIDR,10.0.0.0/8,DIRECT",
|
||||
"IP-CIDR,172.16.0.0/12,DIRECT",
|
||||
"MATCH,PROXY",
|
||||
}
|
||||
rules = append(rules, defaultRules...)
|
||||
}
|
||||
|
||||
// 启用IPv6支持
|
||||
if options.IPv6 {
|
||||
ipv6Rules := []string{
|
||||
"IP-CIDR6,::1/128,DIRECT",
|
||||
"IP-CIDR6,fc00::/7,DIRECT",
|
||||
}
|
||||
rules = append(ipv6Rules, rules...)
|
||||
}
|
||||
|
||||
// 启用局域网支持
|
||||
if options.EnableLan {
|
||||
lanRules := []string{
|
||||
"SRC-IP-CIDR,192.168.0.0/16,DIRECT",
|
||||
"SRC-IP-CIDR,10.0.0.0/8,DIRECT",
|
||||
"SRC-IP-CIDR,172.16.0.0/12,DIRECT",
|
||||
}
|
||||
rules = append(lanRules, rules...)
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func (g *ClashGenerator) parseCustomGroup(definition string, configs []*parser.ProxyConfig) (map[string]interface{}, error) {
|
||||
values, err := url.ParseQuery(definition)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid group definition: %v", err)
|
||||
}
|
||||
|
||||
name := values.Get("name")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("custom group missing name")
|
||||
}
|
||||
|
||||
groupType := values.Get("type")
|
||||
if groupType == "" {
|
||||
groupType = "select"
|
||||
}
|
||||
|
||||
useProviders := make([]string, 0)
|
||||
if useStr := values.Get("use"); useStr != "" {
|
||||
for _, entry := range strings.Split(useStr, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry != "" {
|
||||
useProviders = append(useProviders, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxies := make([]string, 0)
|
||||
if proxyStr := values.Get("proxies"); proxyStr != "" {
|
||||
for _, entry := range strings.Split(proxyStr, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry != "" {
|
||||
proxies = append(proxies, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(proxies) == 0 && len(useProviders) == 0 {
|
||||
for _, cfg := range configs {
|
||||
proxies = append(proxies, cfg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
group := map[string]interface{}{
|
||||
"name": name,
|
||||
"type": groupType,
|
||||
"proxies": proxies,
|
||||
}
|
||||
if len(useProviders) > 0 {
|
||||
group["use"] = useProviders
|
||||
}
|
||||
|
||||
if urlValue := values.Get("url"); urlValue != "" {
|
||||
group["url"] = urlValue
|
||||
}
|
||||
if interval := values.Get("interval"); interval != "" {
|
||||
if n, err := strconv.Atoi(interval); err == nil && n > 0 {
|
||||
group["interval"] = n
|
||||
}
|
||||
}
|
||||
if tolerance := values.Get("tolerance"); tolerance != "" {
|
||||
if n, err := strconv.Atoi(tolerance); err == nil && n >= 0 {
|
||||
group["tolerance"] = n
|
||||
}
|
||||
}
|
||||
if timeout := values.Get("timeout"); timeout != "" {
|
||||
if n, err := strconv.Atoi(timeout); err == nil && n >= 0 {
|
||||
group["timeout"] = n
|
||||
}
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (g *ClashGenerator) applyCustomRulesets(existing []string, options *GenerationOptions) []string {
|
||||
rules := make([]string, 0, len(existing))
|
||||
for _, rule := range existing {
|
||||
rule = strings.TrimSpace(rule)
|
||||
if rule == "" {
|
||||
continue
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
for _, entry := range options.CustomRulesets {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
lower := strings.ToLower(entry)
|
||||
switch {
|
||||
case strings.HasPrefix(lower, "ruleset,"):
|
||||
payload := strings.TrimSpace(entry[len("ruleset,"):])
|
||||
if payload == "" {
|
||||
continue
|
||||
}
|
||||
lines, err := g.loadRulesetLines(options.BasePath, payload)
|
||||
if err != nil {
|
||||
g.logger.WithError(err).Warnf("Failed to load ruleset %s", payload)
|
||||
continue
|
||||
}
|
||||
rules = append(rules, lines...)
|
||||
case strings.HasPrefix(lower, "rule,"):
|
||||
statement := strings.TrimSpace(entry[len("rule,"):])
|
||||
if statement != "" {
|
||||
rules = append(rules, statement)
|
||||
}
|
||||
default:
|
||||
rules = append(rules, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeRules(rules)
|
||||
}
|
||||
|
||||
func (g *ClashGenerator) loadRulesetLines(basePath, relative string) ([]string, error) {
|
||||
path := relative
|
||||
if basePath != "" && !filepath.IsAbs(relative) {
|
||||
path = filepath.Join(basePath, relative)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := strings.ReplaceAll(string(data), "\r\n", "\n")
|
||||
lines := make([]string, 0)
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, trimmed)
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func normalizeRules(rules []string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
result := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
rule = strings.TrimSpace(rule)
|
||||
if rule == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[rule]; ok {
|
||||
continue
|
||||
}
|
||||
seen[rule] = struct{}{}
|
||||
result = append(result, rule)
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user