216 lines
5.2 KiB
Go
216 lines
5.2 KiB
Go
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 := splitCommandTokens(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:" + strings.ToLower(strings.TrimSpace(id))
|
|
}
|
|
return "flatpak"
|
|
case "snap":
|
|
if name := extractRunTarget(clean); name != "" {
|
|
return "snap:" + strings.ToLower(strings.TrimSpace(name))
|
|
}
|
|
return "snap"
|
|
case "gtk-launch":
|
|
// gtk-launch <desktop-id>
|
|
if len(clean) >= 2 {
|
|
id := strings.TrimSpace(clean[1])
|
|
if id != "" && !strings.HasPrefix(id, "-") {
|
|
return "desktop:" + strings.ToLower(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 strings.ToLower(strings.TrimSpace(b))
|
|
}
|
|
}
|
|
|
|
return strings.ToLower(strings.TrimSpace(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 ""
|
|
}
|
|
|
|
// splitCommandTokens performs lightweight shell-style tokenization.
|
|
// It supports single/double quotes and backslash escaping which is enough
|
|
// for canonical app key extraction.
|
|
func splitCommandTokens(raw string) []string {
|
|
s := strings.TrimSpace(raw)
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
|
|
out := make([]string, 0, 8)
|
|
var cur strings.Builder
|
|
inSingle := false
|
|
inDouble := false
|
|
escaped := false
|
|
|
|
flush := func() {
|
|
if cur.Len() == 0 {
|
|
return
|
|
}
|
|
out = append(out, cur.String())
|
|
cur.Reset()
|
|
}
|
|
|
|
for _, r := range s {
|
|
if escaped {
|
|
cur.WriteRune(r)
|
|
escaped = false
|
|
continue
|
|
}
|
|
switch r {
|
|
case '\\':
|
|
if inSingle {
|
|
cur.WriteRune(r)
|
|
} else {
|
|
escaped = true
|
|
}
|
|
case '\'':
|
|
if inDouble {
|
|
cur.WriteRune(r)
|
|
} else {
|
|
inSingle = !inSingle
|
|
}
|
|
case '"':
|
|
if inSingle {
|
|
cur.WriteRune(r)
|
|
} else {
|
|
inDouble = !inDouble
|
|
}
|
|
case ' ', '\t', '\n', '\r':
|
|
if inSingle || inDouble {
|
|
cur.WriteRune(r)
|
|
} else {
|
|
flush()
|
|
}
|
|
default:
|
|
cur.WriteRune(r)
|
|
}
|
|
}
|
|
flush()
|
|
return out
|
|
}
|