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