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