github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  	return d.CacheDir()
   233  	// The container name "keybase" is the group name specified in the entitlement for sandboxed extensions
   234  	// Note: this was added for kbfs finder integration, which was never activated.
   235  	// keybased.sock and kbfsd.sock live in this directory.
   236  	// return d.appDir(d.Home(false), "Library", "keybase", "Library", "Caches")
   237  }
   238  func (d Darwin) ConfigDir() string {
   239  	return d.appDir(d.sharedHome(), "Library", "Application Support")
   240  }
   241  func (d Darwin) DataDir() string {
   242  	return d.appDir(d.Home(false), "Library", "Application Support")
   243  }
   244  func (d Darwin) SharedDataDir() string {
   245  	return d.appDir(d.sharedHome(), "Library", "Application Support")
   246  }
   247  func (d Darwin) RuntimeDir() string               { return d.CacheDir() }
   248  func (d Darwin) ServiceSpawnDir() (string, error) { return d.RuntimeDir(), nil }
   249  func (d Darwin) LogDir() string {
   250  	appName := toUpper(d.appName)
   251  	runMode := d.getRunMode()
   252  	dirs := []string{d.Home(false), "Library", "Logs"}
   253  	if runMode != ProductionRunMode {
   254  		dirs = append(dirs, appName+toUpper(string(runMode)))
   255  	}
   256  	return filepath.Join(dirs...)
   257  }
   258  
   259  func (d Darwin) InfoDir() string {
   260  	// If the user is explicitly passing in a HomeDirectory, make the PID file directory
   261  	// local to that HomeDir. This way it's possible to have multiple keybases in parallel
   262  	// running for a given run mode, without having to explicitly specify a PID file.
   263  	if d.getHome() != "" {
   264  		return d.CacheDir()
   265  	}
   266  	return d.appDir(os.TempDir())
   267  }
   268  
   269  func (d Darwin) DownloadsDir() string {
   270  	return filepath.Join(d.Home(false), "Downloads")
   271  }
   272  
   273  func (d Darwin) Home(emptyOk bool) string {
   274  	ret := d.getHome()
   275  	if len(ret) == 0 && !emptyOk {
   276  		ret = d.getenv("HOME")
   277  	}
   278  	return ret
   279  }
   280  
   281  func (d Darwin) MobileSharedHome(emptyOk bool) string {
   282  	var ret string
   283  	if d.getMobileSharedHome != nil {
   284  		ret = d.getMobileSharedHome()
   285  	}
   286  	if len(ret) == 0 && !emptyOk {
   287  		ret = d.getenv("MOBILE_SHARED_HOME")
   288  	}
   289  	return ret
   290  }
   291  
   292  func (d Darwin) Normalize(s string) string { return s }
   293  
   294  type Win32 struct {
   295  	Base
   296  }
   297  
   298  var win32SplitRE = regexp.MustCompile(`[/\\]`)
   299  
   300  func (w Win32) Split(s string) []string {
   301  	return win32SplitRE.Split(s, -1)
   302  }
   303  
   304  func (w Win32) Unsplit(v []string) string {
   305  	if len(v) > 0 && len(v[0]) == 0 {
   306  		v2 := make([]string, len(v))
   307  		copy(v2, v)
   308  		v[0] = string(filepath.Separator)
   309  	}
   310  	result := filepath.Join(v...)
   311  	// filepath.Join doesn't add a separator on Windows after the drive
   312  	if len(v) > 0 && result[len(v[0])] != filepath.Separator {
   313  		v = append(v[:1], v...)
   314  		v[1] = string(filepath.Separator)
   315  		result = filepath.Join(v...)
   316  	}
   317  	return result
   318  }
   319  
   320  func (w Win32) Normalize(s string) string {
   321  	return w.Unsplit(w.Split(s))
   322  }
   323  
   324  func (w Win32) CacheDir() string                 { return w.Home(false) }
   325  func (w Win32) SharedCacheDir() string           { return w.CacheDir() }
   326  func (w Win32) SandboxCacheDir() string          { return "" } // Unsupported
   327  func (w Win32) ConfigDir() string                { return w.Home(false) }
   328  func (w Win32) DataDir() string                  { return w.Home(false) }
   329  func (w Win32) SharedDataDir() string            { return w.DataDir() }
   330  func (w Win32) RuntimeDir() string               { return w.Home(false) }
   331  func (w Win32) InfoDir() string                  { return w.RuntimeDir() }
   332  func (w Win32) ServiceSpawnDir() (string, error) { return w.RuntimeDir(), nil }
   333  func (w Win32) LogDir() string                   { return w.Home(false) }
   334  
   335  func (w Win32) deriveFromTemp() (ret string) {
   336  	tmp := w.getenv("TEMP")
   337  	if len(tmp) == 0 {
   338  		w.getLog().Info("No 'TEMP' environment variable found")
   339  		tmp = w.getenv("TMP")
   340  		if len(tmp) == 0 {
   341  			w.getLog().Fatalf("No 'TMP' environment variable found")
   342  		}
   343  	}
   344  	v := w.Split(tmp)
   345  	if len(v) < 2 {
   346  		w.getLog().Fatalf("Bad 'TEMP' variable found, no directory separators!")
   347  	}
   348  	last := strings.ToLower(v[len(v)-1])
   349  	rest := v[0 : len(v)-1]
   350  	if last != "temp" && last != "tmp" {
   351  		w.getLog().Warning("TEMP directory didn't end in \\Temp: %s", last)
   352  	}
   353  	if strings.ToLower(rest[len(rest)-1]) == "local" {
   354  		rest[len(rest)-1] = "Roaming"
   355  	}
   356  	ret = w.Unsplit(rest)
   357  	return
   358  }
   359  
   360  func (w Win32) DownloadsDir() string {
   361  	// Prefer to use USERPROFILE instead of w.Home() because the latter goes
   362  	// into APPDATA.
   363  	user, err := user.Current()
   364  	if err != nil {
   365  		return filepath.Join(w.Home(false), "Downloads")
   366  	}
   367  	return filepath.Join(user.HomeDir, "Downloads")
   368  }
   369  
   370  func (w Win32) Home(emptyOk bool) string {
   371  	ret := w.getHome()
   372  	if len(ret) == 0 && !emptyOk {
   373  		ret, _ = LocalDataDir()
   374  		if len(ret) == 0 {
   375  			w.getLog().Info("APPDATA environment variable not found")
   376  		}
   377  
   378  	}
   379  	if len(ret) == 0 && !emptyOk {
   380  		ret = w.deriveFromTemp()
   381  	}
   382  
   383  	packageName := "Keybase"
   384  
   385  	if w.getRunMode() == DevelRunMode || w.getRunMode() == StagingRunMode {
   386  		runModeName := string(w.getRunMode())
   387  		if runModeName != "" {
   388  			// Capitalize the first letter
   389  			r, n := utf8.DecodeRuneInString(runModeName)
   390  			runModeName = string(unicode.ToUpper(r)) + runModeName[n:]
   391  			packageName += runModeName
   392  		}
   393  	}
   394  
   395  	ret = filepath.Join(ret, packageName)
   396  
   397  	return ret
   398  }
   399  
   400  func (w Win32) MobileSharedHome(emptyOk bool) string {
   401  	return w.Home(emptyOk)
   402  }
   403  
   404  func NewHomeFinder(appName string, getHomeFromCmd ConfigGetter, getHomeFromConfig ConfigGetter, getMobileSharedHome ConfigGetter,
   405  	osname string, getRunMode RunModeGetter, getLog LogGetter, getenv EnvGetter) HomeFinder {
   406  	base := Base{appName, getHomeFromCmd, getHomeFromConfig, getMobileSharedHome, getRunMode, getLog, getenv}
   407  	switch runtimeGroup(osname) {
   408  	case keybase1.RuntimeGroup_WINDOWSLIKE:
   409  		return Win32{base}
   410  	case keybase1.RuntimeGroup_DARWINLIKE:
   411  		return Darwin{Base: base}
   412  	default:
   413  		return XdgPosix{base}
   414  	}
   415  }