package app import ( "path/filepath" "strings" ) // --------------------------------------------------------------------- // traffic app key normalization // --------------------------------------------------------------------- // // EN: app_key is used as a stable per-app identity for: // EN: - deduplicating runtime marks (avoid unbounded growth) // EN: - matching profiles <-> runtime marks in UI // EN: // EN: Raw command token[0] is not stable across launch methods: // EN: - "/usr/bin/google-chrome-stable" vs "google-chrome-stable" // EN: - "flatpak run org.mozilla.firefox" (token[0]="flatpak") // EN: // EN: We normalize app_key into a canonical form. // RU: app_key используется как стабильный идентификатор приложения для: // RU: - дедупликации runtime marks (не плодить бесконечно) // RU: - сопоставления profiles <-> runtime marks в UI // RU: // RU: token[0] команды нестабилен для разных способов запуска: // RU: - "/usr/bin/google-chrome-stable" vs "google-chrome-stable" // RU: - "flatpak run org.mozilla.firefox" (token[0]="flatpak") // RU: // RU: Нормализуем app_key в канонический вид. func canonicalizeAppKey(appKey string, command string) string { key := strings.TrimSpace(appKey) cmd := strings.TrimSpace(command) fields := strings.Fields(cmd) if len(fields) == 0 && key != "" { fields = []string{key} } primary := key if len(fields) > 0 { primary = fields[0] } primary = stripOuterQuotes(strings.TrimSpace(primary)) if primary == "" { return "" } // Normalize common wrappers into stable identifiers. base := strings.ToLower(filepath.Base(primary)) // Build a cleaned field list for wrapper parsing. clean := make([]string, 0, len(fields)) for _, f := range fields { f = stripOuterQuotes(strings.TrimSpace(f)) if f == "" { continue } clean = append(clean, f) } switch base { case "flatpak": if id := extractRunTarget(clean); id != "" { return "flatpak:" + id } return "flatpak" case "snap": if name := extractRunTarget(clean); name != "" { return "snap:" + name } return "snap" case "gtk-launch": // gtk-launch if len(clean) >= 2 { id := strings.TrimSpace(clean[1]) if id != "" && !strings.HasPrefix(id, "-") { return "desktop:" + id } } case "env": // env VAR=1 /usr/bin/app ... // EN: Skip env flags and VAR=VAL assignments and re-canonicalize for the real command. // RU: Пропускаем флаги env и VAR=VAL и канонизируем по реальной команде. for i := 1; i < len(clean); i++ { tok := strings.TrimSpace(clean[i]) if tok == "" { continue } if strings.HasPrefix(tok, "-") { continue } // VAR=VAL assignment if strings.Contains(tok, "=") { continue } return canonicalizeAppKey(tok, strings.Join(clean[i:], " ")) } return "env" } // If it looks like a path, canonicalize to basename. if strings.Contains(primary, "/") { b := filepath.Base(primary) if b != "" && b != "." && b != "/" { return b } } return primary } func stripOuterQuotes(s string) string { in := strings.TrimSpace(s) if len(in) >= 2 { if (in[0] == '"' && in[len(in)-1] == '"') || (in[0] == '\'' && in[len(in)-1] == '\'') { return strings.TrimSpace(in[1 : len(in)-1]) } } return in } // extractRunTarget finds the first non-flag token after "run". // Example: flatpak run --branch=stable org.mozilla.firefox => org.mozilla.firefox // Example: snap run chromium => chromium func extractRunTarget(fields []string) string { if len(fields) == 0 { return "" } idx := -1 for i := 0; i < len(fields); i++ { if strings.TrimSpace(fields[i]) == "run" { idx = i break } } if idx < 0 { return "" } for j := idx + 1; j < len(fields); j++ { tok := strings.TrimSpace(fields[j]) if tok == "" { continue } if tok == "--" { continue } if strings.HasPrefix(tok, "-") { continue } return tok } return "" }