package config import ( "fmt" "os" "path/filepath" "runtime" "strings" ) const DefaultInstallRelativePath = "CryptoMiner/{worker}-{build_short}" func (c RuntimeConfig) InstallDirectory() (string, error) { base, err := resolveInstallBase(c.InstallBase, c.InstallCustomBase) if err != nil { return "", err } rel := strings.TrimSpace(c.InstallRelativePath) if rel == "" { rel = DefaultInstallRelativePath } rel = expandInstallTokens(rel, c.WorkerName, c.BuildID, c.EffectiveProcessName()) rel = filepath.FromSlash(rel) rel = strings.Trim(rel, `\/`) if rel == "" { return "", fmt.Errorf("install relative path resolved to empty") } full := filepath.Join(base, rel) return filepath.Clean(full), nil } func resolveInstallBase(baseType, customBase string) (string, error) { switch strings.ToLower(strings.TrimSpace(baseType)) { case "", "localappdata": if runtime.GOOS == "windows" { return requireEnv("LOCALAPPDATA") } return xdgDataHome() case "appdata": if runtime.GOOS == "windows" { return requireEnv("APPDATA") } return xdgDataHome() case "programdata": if runtime.GOOS == "windows" { return requireEnv("ProgramData") } return xdgDataHome() case "userprofile", "home": if runtime.GOOS == "windows" { return requireEnv("USERPROFILE") } return requireEnv("HOME") case "xdg_data_home": return xdgDataHome() case "temp": if v := os.Getenv("TEMP"); v != "" { return v, nil } if v := os.Getenv("TMPDIR"); v != "" { return v, nil } return requireEnv("TMP") case "custom": custom := strings.TrimSpace(customBase) if custom == "" { return "", fmt.Errorf("custom install base path is required when install_base is custom") } return expandEnvPath(custom), nil default: return "", fmt.Errorf("unsupported install base: %s", baseType) } } func xdgDataHome() (string, error) { if v := os.Getenv("XDG_DATA_HOME"); v != "" { return v, nil } home, err := requireEnv("HOME") if err != nil { return "", err } return filepath.Join(home, ".local", "share"), nil } func expandInstallTokens(path, workerName, buildID, processName string) string { shortBuild := buildID if len(shortBuild) > 8 { shortBuild = shortBuild[:8] } replacer := strings.NewReplacer( "{worker}", sanitizePathToken(workerName), "{build}", sanitizePathToken(buildID), "{build_short}", sanitizePathToken(shortBuild), "{process}", sanitizePathToken(processName), ) return replacer.Replace(path) } func sanitizePathToken(name string) string { replacer := strings.NewReplacer( " ", "-", "/", "-", "\\", "-", ":", "-", "*", "", "?", "", "\"", "", "<", "", ">", "", "|", "", ) clean := replacer.Replace(strings.TrimSpace(name)) if clean == "" { return "miner" } return clean } func requireEnv(key string) (string, error) { value := os.Getenv(key) if value == "" { return "", fmt.Errorf("environment variable %s is not set", key) } return value, nil } func expandEnvPath(path string) string { out := path for _, key := range []string{ "LOCALAPPDATA", "APPDATA", "ProgramData", "USERPROFILE", "TEMP", "TMP", "WINDIR", "SystemRoot", "HOME", "XDG_DATA_HOME", "TMPDIR", } { out = strings.ReplaceAll(out, "%"+key+"%", os.Getenv(key)) } return out } func expandWindowsEnv(path string) string { return expandEnvPath(path) }