package proxy import ( "encoding/json" "regexp" "strings" "github.com/peterqiu0516/sub-store/internal/model" "github.com/peterqiu0516/sub-store/internal/util" "gopkg.in/yaml.v3" ) var ( // reURIScheme matches a URI scheme like "vless://", "ss://", etc. reURIScheme = regexp.MustCompile(`(?im)^[a-z][a-z0-9+.-]*://`) // reYamlKeys matches YAML top-level keys: proxies, proxy-groups, rules reYamlKeys = regexp.MustCompile(`(?m)^\s*(proxies|proxy-groups|rules)\s*:`) // reJSONStart matches the start of a JSON array or object reJSONStart = regexp.MustCompile(`^\s*[\[{]`) // reQxConfig matches QX-style config lines: "shadowsocks = ...", "vmess = ...", etc. reQxConfig = regexp.MustCompile(`(?im)^\s*(shadowsocks|vmess|vless|trojan|http|socks5|anytls)\s*=`) // reNamedConfig matches Surge/Loon named config lines reNamedConfig = regexp.MustCompile(`(?im)^\s*[^=\n]{1,80}\s*=\s*(ss|shadowsocks|ssr|vmess|vless|trojan|http|https|socks5|socks5-tls|hysteria2|hysteria|anytls|tuic|tuic-v5)\s*,`) // reCommentOrSection matches lines to skip: comments (#, ;) and section headers ([...]) reCommentOrSection = regexp.MustCompile(`^\s*(#|;|\[[^\]]+\])`) ) // ParseProxies is the main entry point for parsing subscription content. // It trims the input, detects JSON/YAML/URI-line format, and dispatches accordingly. func ParseProxies(raw string) []model.ProxyNode { text := strings.TrimSpace(raw) if text == "" { return nil } if reJSONStart.MatchString(text) { return ParseJsonProxies(text) } if reYamlKeys.MatchString(text) { return ParseYamlProxies(text) } return ParseProxyLines(text) } // DecodeMaybeBase64 tries structured detection first; if the input doesn't // look like a structured subscription, attempts base64 decode. func DecodeMaybeBase64(raw string) string { text := strings.TrimSpace(raw) if LooksLikeStructuredSubscription(text) { return raw } // Try base64 decode (strip all whitespace first) cleaned := regexp.MustCompile(`\s+`).ReplaceAllString(text, "") decoded, err := util.DecodeBase64Auto(cleaned) if err != nil { return raw } if LooksLikeStructuredSubscription(strings.TrimSpace(decoded)) { return decoded } return raw } // LooksLikeStructuredSubscription checks if the text looks like structured // subscription content (URI scheme, YAML keys, JSON, or client config lines). func LooksLikeStructuredSubscription(text string) bool { return reURIScheme.MatchString(text) || reYamlKeys.MatchString(text) || reJSONStart.MatchString(text) || reQxConfig.MatchString(text) || reNamedConfig.MatchString(text) } // ParseJsonProxies parses a JSON array of proxies or {"proxies": [...]}. func ParseJsonProxies(raw string) []model.ProxyNode { var payload any if err := json.Unmarshal([]byte(raw), &payload); err != nil { return nil } var rawList []any switch v := payload.(type) { case []any: rawList = v case map[string]any: if arr, ok := v["proxies"].([]any); ok { rawList = arr } default: return nil } var result []model.ProxyNode for _, item := range rawList { if m, ok := item.(map[string]any); ok { normalized := NormalizeProxy(m) if IsProxyNode(normalized) { result = append(result, normalized) } } } return result } // ParseYamlProxies parses a YAML document with a "proxies:" key. func ParseYamlProxies(raw string) []model.ProxyNode { var payload map[string]any if err := yaml.Unmarshal([]byte(raw), &payload); err != nil { return nil } rawList, ok := payload["proxies"].([]any) if !ok { return nil } var result []model.ProxyNode for _, item := range rawList { if m, ok := item.(map[string]any); ok { normalized := NormalizeProxy(m) if IsProxyNode(normalized) { result = append(result, normalized) } } } return result } // ParseProxyLines splits raw text into lines, skips comments/blanks/section headers, // and parses each line as a URI or client config line. // // Literal "\n" / "\r\n" sequences (backslash + n, as produced by some frontends // stuffing multi-line content into a single string) are normalized to real // newlines before splitting so each link is parsed as its own line. func ParseProxyLines(raw string) []model.ProxyNode { if strings.Contains(raw, `\n`) { raw = strings.ReplaceAll(raw, `\r\n`, "\n") raw = strings.ReplaceAll(raw, `\n`, "\n") } lines := strings.Split(raw, "\n") var result []model.ProxyNode index := 0 for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { continue } if reCommentOrSection.MatchString(line) { continue } var node model.ProxyNode if node = ParseProxyUri(line, index); node == nil { node = ParseClientProxyLine(line, index) } if node != nil { result = append(result, node) index++ } } return result }