go4.org@v0.0.0-20230225012048-214862532bf5/xdgdir/xdgdir.go (about)

     1  /*
     2  Copyright 2017 The go4 Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations 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
    22  package xdgdir // import "go4.org/xdgdir"
    23  
    24  import (
    25  	"errors"
    26  	"fmt"
    27  	"os"
    28  	"os/user"
    29  	"path/filepath"
    30  	"syscall"
    31  )
    32  
    33  // Directories defined by the specification.
    34  var (
    35  	Data    Dir
    36  	Config  Dir
    37  	Cache   Dir
    38  	Runtime Dir
    39  )
    40  
    41  func 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.
    67  type 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.
    92  func (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.
   102  func (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  
   123  func (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.
   143  func (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.
   174  func (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.
   204  func (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  
   219  func 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.
   225  var 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.
   237  var getenv = os.Getenv
   238  
   239  // geteuid retrieves the effective user ID of the process.  It can be faked for testing.
   240  var geteuid = os.Geteuid