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  }