github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/home.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package libkb 5 6 import ( 7 "fmt" 8 "os" 9 "os/user" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strings" 14 "unicode" 15 "unicode/utf8" 16 17 "github.com/keybase/client/go/protocol/keybase1" 18 ) 19 20 type ConfigGetter func() string 21 type RunModeGetter func() RunMode 22 type EnvGetter func(s string) string 23 24 type Base struct { 25 appName string 26 getHomeFromCmd ConfigGetter 27 getHomeFromConfig ConfigGetter 28 getMobileSharedHome ConfigGetter 29 getRunMode RunModeGetter 30 getLog LogGetter 31 getenvFunc EnvGetter 32 } 33 34 type HomeFinder interface { 35 CacheDir() string 36 SharedCacheDir() string 37 ConfigDir() string 38 DownloadsDir() string 39 Home(emptyOk bool) string 40 MobileSharedHome(emptyOk bool) string 41 DataDir() string 42 SharedDataDir() string 43 RuntimeDir() string 44 Normalize(s string) string 45 LogDir() string 46 ServiceSpawnDir() (string, error) 47 SandboxCacheDir() string // For macOS 48 InfoDir() string 49 IsNonstandardHome() (bool, error) 50 } 51 52 func (b Base) getHome() string { 53 if b.getHomeFromCmd != nil { 54 ret := b.getHomeFromCmd() 55 if ret != "" { 56 return ret 57 } 58 } 59 if b.getHomeFromConfig != nil { 60 ret := b.getHomeFromConfig() 61 if ret != "" { 62 return ret 63 } 64 } 65 return "" 66 } 67 68 func (b Base) IsNonstandardHome() (bool, error) { 69 return false, fmt.Errorf("unsupported on %s", runtime.GOOS) 70 } 71 72 func (b Base) getenv(s string) string { 73 if b.getenvFunc != nil { 74 return b.getenvFunc(s) 75 } 76 return os.Getenv(s) 77 } 78 79 func (b Base) Join(elem ...string) string { return filepath.Join(elem...) } 80 81 type XdgPosix struct { 82 Base 83 } 84 85 func (x XdgPosix) Normalize(s string) string { return s } 86 87 func (x XdgPosix) Home(emptyOk bool) string { 88 ret := x.getHome() 89 if len(ret) == 0 && !emptyOk { 90 ret = x.getenv("HOME") 91 } 92 if ret == "" { 93 return "" 94 } 95 resolved, err := filepath.Abs(ret) 96 if err != nil { 97 return ret 98 } 99 return resolved 100 } 101 102 // IsNonstandardHome is true if the home directory gleaned via cmdline, 103 // env, or config is different from that in /etc/passwd. 104 func (x XdgPosix) IsNonstandardHome() (bool, error) { 105 passed := x.Home(false) 106 if passed == "" { 107 return false, nil 108 } 109 passwd, err := user.Current() 110 if err != nil { 111 return false, err 112 } 113 passwdAbs, err := filepath.Abs(passwd.HomeDir) 114 if err != nil { 115 return false, err 116 } 117 passedAbs, err := filepath.Abs(passed) 118 if err != nil { 119 return false, err 120 } 121 return passedAbs != passwdAbs, nil 122 } 123 124 func (x XdgPosix) MobileSharedHome(emptyOk bool) string { 125 return x.Home(emptyOk) 126 } 127 128 func (x XdgPosix) dirHelper(xdgEnvVar string, prefixDirs ...string) string { 129 appName := x.appName 130 if x.getRunMode() != ProductionRunMode { 131 appName = appName + "." + string(x.getRunMode()) 132 } 133 134 isNonstandard, isNonstandardErr := x.IsNonstandardHome() 135 xdgSpecified := x.getenv(xdgEnvVar) 136 137 // If the user specified a nonstandard home directory, or there's no XDG 138 // environment variable present, use the home directory from the 139 // commandline/environment/config. 140 if (isNonstandardErr == nil && isNonstandard) || xdgSpecified == "" { 141 alternateDir := x.Join(append([]string{x.Home(false)}, prefixDirs...)...) 142 return x.Join(alternateDir, appName) 143 } 144 145 // Otherwise, use the XDG standard. 146 return x.Join(xdgSpecified, appName) 147 } 148 149 func (x XdgPosix) ConfigDir() string { return x.dirHelper("XDG_CONFIG_HOME", ".config") } 150 func (x XdgPosix) CacheDir() string { return x.dirHelper("XDG_CACHE_HOME", ".cache") } 151 func (x XdgPosix) SharedCacheDir() string { return x.CacheDir() } 152 func (x XdgPosix) SandboxCacheDir() string { return "" } // Unsupported 153 func (x XdgPosix) DataDir() string { return x.dirHelper("XDG_DATA_HOME", ".local", "share") } 154 func (x XdgPosix) SharedDataDir() string { return x.DataDir() } 155 func (x XdgPosix) DownloadsDir() string { 156 xdgSpecified := x.getenv("XDG_DOWNLOAD_DIR") 157 if xdgSpecified != "" { 158 return xdgSpecified 159 } 160 return filepath.Join(x.Home(false), "Downloads") 161 } 162 func (x XdgPosix) RuntimeDir() string { return x.dirHelper("XDG_RUNTIME_DIR", ".config") } 163 func (x XdgPosix) InfoDir() string { return x.RuntimeDir() } 164 165 func (x XdgPosix) ServiceSpawnDir() (ret string, err error) { 166 ret = x.RuntimeDir() 167 if len(ret) == 0 { 168 ret, err = os.MkdirTemp("", "keybase_service") 169 } 170 return 171 } 172 173 func (x XdgPosix) LogDir() string { 174 // There doesn't seem to be an official place for logs in the XDG spec, but 175 // according to http://stackoverflow.com/a/27965014/823869 at least, this 176 // is the best compromise. 177 return x.CacheDir() 178 } 179 180 type Darwin struct { 181 Base 182 forceIOS bool // for testing 183 } 184 185 func toUpper(s string) string { 186 if s == "" { 187 return s 188 } 189 a := []rune(s) 190 a[0] = unicode.ToUpper(a[0]) 191 return string(a) 192 } 193 194 func (d Darwin) isIOS() bool { 195 return isIOS || d.forceIOS 196 } 197 198 func (d Darwin) appDir(dirs ...string) string { 199 appName := toUpper(d.appName) 200 runMode := d.getRunMode() 201 if runMode != ProductionRunMode { 202 appName += toUpper(string(runMode)) 203 } 204 dirs = append(dirs, appName) 205 return filepath.Join(dirs...) 206 } 207 208 func (d Darwin) sharedHome() string { 209 homeDir := d.Home(false) 210 if d.isIOS() { 211 // check if we have a shared container path, and if so, that is where the shared home is. 212 sharedHome := d.getMobileSharedHome() 213 if len(sharedHome) > 0 { 214 homeDir = sharedHome 215 } 216 } 217 return homeDir 218 } 219 220 func (d Darwin) CacheDir() string { 221 return d.appDir(d.Home(false), "Library", "Caches") 222 } 223 224 func (d Darwin) SharedCacheDir() string { 225 return d.appDir(d.sharedHome(), "Library", "Caches") 226 } 227 228 func (d Darwin) SandboxCacheDir() string { 229 if d.isIOS() { 230 return "" 231 } 232 // The container name "keybase" is the group name specified in the entitlement for sandboxed extensions 233 // Note: this was added for kbfs finder integration, which was never activated. 234 // keybased.sock and kbfsd.sock live in this directory. 235 return d.appDir(d.Home(false), "Library", "Group Containers", "keybase", "Library", "Caches") 236 } 237 func (d Darwin) ConfigDir() string { 238 return d.appDir(d.sharedHome(), "Library", "Application Support") 239 } 240 func (d Darwin) DataDir() string { 241 return d.appDir(d.Home(false), "Library", "Application Support") 242 } 243 func (d Darwin) SharedDataDir() string { 244 return d.appDir(d.sharedHome(), "Library", "Application Support") 245 } 246 func (d Darwin) RuntimeDir() string { return d.CacheDir() } 247 func (d Darwin) ServiceSpawnDir() (string, error) { return d.RuntimeDir(), nil } 248 func (d Darwin) LogDir() string { 249 appName := toUpper(d.appName) 250 runMode := d.getRunMode() 251 dirs := []string{d.Home(false), "Library", "Logs"} 252 if runMode != ProductionRunMode { 253 dirs = append(dirs, appName+toUpper(string(runMode))) 254 } 255 return filepath.Join(dirs...) 256 } 257 258 func (d Darwin) InfoDir() string { 259 // If the user is explicitly passing in a HomeDirectory, make the PID file directory 260 // local to that HomeDir. This way it's possible to have multiple keybases in parallel 261 // running for a given run mode, without having to explicitly specify a PID file. 262 if d.getHome() != "" { 263 return d.CacheDir() 264 } 265 return d.appDir(os.TempDir()) 266 } 267 268 func (d Darwin) DownloadsDir() string { 269 return filepath.Join(d.Home(false), "Downloads") 270 } 271 272 func (d Darwin) Home(emptyOk bool) string { 273 ret := d.getHome() 274 if len(ret) == 0 && !emptyOk { 275 ret = d.getenv("HOME") 276 } 277 return ret 278 } 279 280 func (d Darwin) MobileSharedHome(emptyOk bool) string { 281 var ret string 282 if d.getMobileSharedHome != nil { 283 ret = d.getMobileSharedHome() 284 } 285 if len(ret) == 0 && !emptyOk { 286 ret = d.getenv("MOBILE_SHARED_HOME") 287 } 288 return ret 289 } 290 291 func (d Darwin) Normalize(s string) string { return s } 292 293 type Win32 struct { 294 Base 295 } 296 297 var win32SplitRE = regexp.MustCompile(`[/\\]`) 298 299 func (w Win32) Split(s string) []string { 300 return win32SplitRE.Split(s, -1) 301 } 302 303 func (w Win32) Unsplit(v []string) string { 304 if len(v) > 0 && len(v[0]) == 0 { 305 v2 := make([]string, len(v)) 306 copy(v2, v) 307 v[0] = string(filepath.Separator) 308 } 309 result := filepath.Join(v...) 310 // filepath.Join doesn't add a separator on Windows after the drive 311 if len(v) > 0 && result[len(v[0])] != filepath.Separator { 312 v = append(v[:1], v...) 313 v[1] = string(filepath.Separator) 314 result = filepath.Join(v...) 315 } 316 return result 317 } 318 319 func (w Win32) Normalize(s string) string { 320 return w.Unsplit(w.Split(s)) 321 } 322 323 func (w Win32) CacheDir() string { return w.Home(false) } 324 func (w Win32) SharedCacheDir() string { return w.CacheDir() } 325 func (w Win32) SandboxCacheDir() string { return "" } // Unsupported 326 func (w Win32) ConfigDir() string { return w.Home(false) } 327 func (w Win32) DataDir() string { return w.Home(false) } 328 func (w Win32) SharedDataDir() string { return w.DataDir() } 329 func (w Win32) RuntimeDir() string { return w.Home(false) } 330 func (w Win32) InfoDir() string { return w.RuntimeDir() } 331 func (w Win32) ServiceSpawnDir() (string, error) { return w.RuntimeDir(), nil } 332 func (w Win32) LogDir() string { return w.Home(false) } 333 334 func (w Win32) deriveFromTemp() (ret string) { 335 tmp := w.getenv("TEMP") 336 if len(tmp) == 0 { 337 w.getLog().Info("No 'TEMP' environment variable found") 338 tmp = w.getenv("TMP") 339 if len(tmp) == 0 { 340 w.getLog().Fatalf("No 'TMP' environment variable found") 341 } 342 } 343 v := w.Split(tmp) 344 if len(v) < 2 { 345 w.getLog().Fatalf("Bad 'TEMP' variable found, no directory separators!") 346 } 347 last := strings.ToLower(v[len(v)-1]) 348 rest := v[0 : len(v)-1] 349 if last != "temp" && last != "tmp" { 350 w.getLog().Warning("TEMP directory didn't end in \\Temp: %s", last) 351 } 352 if strings.ToLower(rest[len(rest)-1]) == "local" { 353 rest[len(rest)-1] = "Roaming" 354 } 355 ret = w.Unsplit(rest) 356 return 357 } 358 359 func (w Win32) DownloadsDir() string { 360 // Prefer to use USERPROFILE instead of w.Home() because the latter goes 361 // into APPDATA. 362 user, err := user.Current() 363 if err != nil { 364 return filepath.Join(w.Home(false), "Downloads") 365 } 366 return filepath.Join(user.HomeDir, "Downloads") 367 } 368 369 func (w Win32) Home(emptyOk bool) string { 370 ret := w.getHome() 371 if len(ret) == 0 && !emptyOk { 372 ret, _ = LocalDataDir() 373 if len(ret) == 0 { 374 w.getLog().Info("APPDATA environment variable not found") 375 } 376 377 } 378 if len(ret) == 0 && !emptyOk { 379 ret = w.deriveFromTemp() 380 } 381 382 packageName := "Keybase" 383 384 if w.getRunMode() == DevelRunMode || w.getRunMode() == StagingRunMode { 385 runModeName := string(w.getRunMode()) 386 if runModeName != "" { 387 // Capitalize the first letter 388 r, n := utf8.DecodeRuneInString(runModeName) 389 runModeName = string(unicode.ToUpper(r)) + runModeName[n:] 390 packageName += runModeName 391 } 392 } 393 394 ret = filepath.Join(ret, packageName) 395 396 return ret 397 } 398 399 func (w Win32) MobileSharedHome(emptyOk bool) string { 400 return w.Home(emptyOk) 401 } 402 403 func NewHomeFinder(appName string, getHomeFromCmd ConfigGetter, getHomeFromConfig ConfigGetter, getMobileSharedHome ConfigGetter, 404 osname string, getRunMode RunModeGetter, getLog LogGetter, getenv EnvGetter) HomeFinder { 405 base := Base{appName, getHomeFromCmd, getHomeFromConfig, getMobileSharedHome, getRunMode, getLog, getenv} 406 switch runtimeGroup(osname) { 407 case keybase1.RuntimeGroup_WINDOWSLIKE: 408 return Win32{base} 409 case keybase1.RuntimeGroup_DARWINLIKE: 410 return Darwin{Base: base} 411 default: 412 return XdgPosix{base} 413 } 414 }