package transportcfg import ( "crypto/sha256" "encoding/hex" "encoding/json" "os" "path/filepath" "reflect" "sort" "strconv" "strings" "time" ) type ConfigDiff struct { Added int Removed int Changed int } func SingBoxTypedProtocolSupported(proto string) bool { switch strings.ToLower(strings.TrimSpace(proto)) { case "vless", "trojan", "shadowsocks", "wireguard", "hysteria2", "tuic": return true default: return false } } func ParsePort(v any) (int, bool) { switch x := v.(type) { case int: return x, true case int64: return int(x), true case float64: return int(x), true case string: s := strings.TrimSpace(x) if s == "" { return 0, false } n, err := strconv.Atoi(s) if err != nil { return 0, false } return n, true default: return 0, false } } func DigestJSONMap(config map[string]any) string { if config == nil { return "" } b, err := json.Marshal(config) if err != nil { return "" } h := sha256.Sum256(b) return hex.EncodeToString(h[:]) } func DiffConfigMaps(prev, next map[string]any) (ConfigDiff, bool) { diff := ConfigDiff{} changed := false if prev == nil { diff.Added = len(next) return diff, diff.Added > 0 } seen := map[string]struct{}{} for k, pv := range prev { seen[k] = struct{}{} nv, ok := next[k] if !ok { diff.Removed++ changed = true continue } if !reflect.DeepEqual(pv, nv) { diff.Changed++ changed = true } } for k := range next { if _, ok := seen[k]; ok { continue } diff.Added++ changed = true } return diff, changed } func ProfileConfigPath(rootDir, profileID string, sanitizeID func(string) string) string { id := profileID if sanitizeID != nil { id = sanitizeID(id) } if strings.TrimSpace(id) == "" { id = "profile" } return filepath.Join(strings.TrimSpace(rootDir), id+".json") } func WriteJSONConfigFile(path string, config map[string]any) error { data, err := json.MarshalIndent(config, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, append(data, '\n'), 0o644); err != nil { return err } return os.Rename(tmp, path) } func ReadJSONMapFile(path string) map[string]any { data, err := os.ReadFile(path) if err != nil { return nil } var out map[string]any if err := json.Unmarshal(data, &out); err != nil { return nil } return out } func ReadFileOptional(path string) ([]byte, bool, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, false, nil } return nil, false, err } return data, true, nil } func RestoreFileOptional(path string, data []byte, exists bool) error { if !exists { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return err } return nil } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } return os.Rename(tmp, path) } func SanitizeHistoryStamp(ts string, now time.Time) string { s := strings.TrimSpace(ts) if s == "" { s = now.UTC().Format(time.RFC3339Nano) } repl := strings.NewReplacer(":", "", "-", "", "T", "_", "Z", "", ".", "") out := repl.Replace(s) out = strings.Trim(out, "_") if out == "" { out = strconv.FormatInt(now.UTC().UnixNano(), 10) } return out } func JoinMessages(messages []string) string { if len(messages) == 0 { return "" } parts := make([]string, 0, len(messages)) for _, msg := range messages { v := strings.TrimSpace(msg) if v == "" { continue } parts = append(parts, v) } return strings.Join(parts, "; ") } func SortRecordsDescByAt[T any](items []T, extractAt func(T) string) { if len(items) < 2 || extractAt == nil { return } sort.Slice(items, func(i, j int) bool { return extractAt(items[i]) > extractAt(items[j]) }) }