1/*
2Copyright 2017 The go4 Authors
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8     http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// Package xdgdir implements the Free Desktop Base Directory
18// specification for locating directories.
19//
20// The specification is at
21// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
22package xdgdir // import "go4.org/xdgdir"
23
24import (
25	"errors"
26	"fmt"
27	"os"
28	"os/user"
29	"path/filepath"
30	"syscall"
31)
32
33// Directories defined by the specification.
34var (
35	Data    Dir
36	Config  Dir
37	Cache   Dir
38	Runtime Dir
39)
40
41func init() {
42	// Placed in init for the sake of readable docs.
43	Data = Dir{
44		env:          "XDG_DATA_HOME",
45		dirsEnv:      "XDG_DATA_DIRS",
46		fallback:     ".local/share",
47		dirsFallback: []string{"/usr/local/share", "/usr/share"},
48	}
49	Config = Dir{
50		env:          "XDG_CONFIG_HOME",
51		dirsEnv:      "XDG_CONFIG_DIRS",
52		fallback:     ".config",
53		dirsFallback: []string{"/etc/xdg"},
54	}
55	Cache = Dir{
56		env:      "XDG_CACHE_HOME",
57		fallback: ".cache",
58	}
59	Runtime = Dir{
60		env:       "XDG_RUNTIME_DIR",
61		userOwned: true,
62	}
63}
64
65// A Dir is a logical base directory along with additional search
66// directories.
67type Dir struct {
68	// env is the name of the environment variable for the base directory
69	// relative to which files should be written.
70	env string
71
72	// dirsEnv is the name of the environment variable containing
73	// preference-ordered base directories to search for files.
74	dirsEnv string
75
76	// fallback is the home-relative path to use if the variable named by
77	// env is not set.
78	fallback string
79
80	// dirsFallback is the list of paths to use if the variable named by
81	// dirsEnv is not set.
82	dirsFallback []string
83
84	// If userOwned is true, then for the directory to be considered
85	// valid, it must be owned by the user with the mode 700.  This is
86	// only used for XDG_RUNTIME_DIR.
87	userOwned bool
88}
89
90// String returns the name of the primary environment variable for the
91// directory.
92func (d Dir) String() string {
93	if d.env == "" {
94		panic("xdgdir.Dir.String() on zero Dir")
95	}
96	return d.env
97}
98
99// Path returns the absolute path of the primary directory, or an empty
100// string if there's no suitable directory present.  This is the path
101// that should be used for writing files.
102func (d Dir) Path() string {
103	if d.env == "" {
104		panic("xdgdir.Dir.Path() on zero Dir")
105	}
106	p := d.path()
107	if p != "" && d.userOwned {
108		info, err := os.Stat(p)
109		if err != nil {
110			return ""
111		}
112		if !info.IsDir() || info.Mode().Perm() != 0700 {
113			return ""
114		}
115		st, ok := info.Sys().(*syscall.Stat_t)
116		if !ok || int(st.Uid) != geteuid() {
117			return ""
118		}
119	}
120	return p
121}
122
123func (d Dir) path() string {
124	if e := getenv(d.env); isValidPath(e) {
125		return e
126	}
127	if d.fallback == "" {
128		return ""
129	}
130	home := findHome()
131	if home == "" {
132		return ""
133	}
134	p := filepath.Join(home, d.fallback)
135	if !isValidPath(p) {
136		return ""
137	}
138	return p
139}
140
141// SearchPaths returns the list of paths (in descending order of
142// preference) to search for files.
143func (d Dir) SearchPaths() []string {
144	if d.env == "" {
145		panic("xdgdir.Dir.SearchPaths() on zero Dir")
146	}
147	var paths []string
148	if p := d.Path(); p != "" {
149		paths = append(paths, p)
150	}
151	if d.dirsEnv == "" {
152		return paths
153	}
154	e := getenv(d.dirsEnv)
155	if e == "" {
156		paths = append(paths, d.dirsFallback...)
157		return paths
158	}
159	epaths := filepath.SplitList(e)
160	n := 0
161	for _, p := range epaths {
162		if isValidPath(p) {
163			epaths[n] = p
164			n++
165		}
166	}
167	paths = append(paths, epaths[:n]...)
168	return paths
169}
170
171// Open opens the named file inside the directory for reading.  If the
172// directory has multiple search paths, each path is checked in order
173// for the file and the first one found is opened.
174func (d Dir) Open(name string) (*os.File, error) {
175	if d.env == "" {
176		return nil, errors.New("xdgdir: Open on zero Dir")
177	}
178	paths := d.SearchPaths()
179	if len(paths) == 0 {
180		return nil, fmt.Errorf("xdgdir: open %s: %s is invalid or not set", name, d.env)
181	}
182	var firstErr error
183	for _, p := range paths {
184		f, err := os.Open(filepath.Join(p, name))
185		if err == nil {
186			return f, nil
187		} else if !os.IsNotExist(err) {
188			firstErr = err
189		}
190	}
191	if firstErr != nil {
192		return nil, firstErr
193	}
194	return nil, &os.PathError{
195		Op:   "Open",
196		Path: filepath.Join("$"+d.env, name),
197		Err:  os.ErrNotExist,
198	}
199}
200
201// Create creates the named file inside the directory mode 0666 (before
202// umask), truncating it if it already exists.  Parent directories of
203// the file will be created with mode 0700.
204func (d Dir) Create(name string) (*os.File, error) {
205	if d.env == "" {
206		return nil, errors.New("xdgdir: Create on zero Dir")
207	}
208	p := d.Path()
209	if p == "" {
210		return nil, fmt.Errorf("xdgdir: create %s: %s is invalid or not set", name, d.env)
211	}
212	fp := filepath.Join(p, name)
213	if err := os.MkdirAll(filepath.Dir(fp), 0700); err != nil {
214		return nil, err
215	}
216	return os.Create(fp)
217}
218
219func isValidPath(path string) bool {
220	return path != "" && filepath.IsAbs(path)
221}
222
223// findHome returns the user's home directory or the empty string if it
224// can't be found.  It can be faked for testing.
225var findHome = func() string {
226	if h := getenv("HOME"); h != "" {
227		return h
228	}
229	u, err := user.Current()
230	if err != nil {
231		return ""
232	}
233	return u.HomeDir
234}
235
236// getenv retrieves an environment variable.  It can be faked for testing.
237var getenv = os.Getenv
238
239// geteuid retrieves the effective user ID of the process.  It can be faked for testing.
240var geteuid = os.Geteuid
241