chore: sub-store Go 重写项目初始化
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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.
|
||||
func ParseProxyLines(raw string) []model.ProxyNode {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user