code.gitea.io/gitea@v1.22.3/modules/util/path.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package util
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"net/url"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"regexp"
    14  	"runtime"
    15  	"strings"
    16  )
    17  
    18  // PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately.
    19  // It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed.
    20  // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
    21  //
    22  //	empty => ``
    23  //	`` => ``
    24  //	`..` => `.`
    25  //	`dir` => `dir`
    26  //	`/dir/` => `dir`
    27  //	`foo\..\bar` => `foo\..\bar`
    28  //	{`foo`, ``, `bar`} => `foo/bar`
    29  //	{`foo`, `..`, `bar`} => `foo/bar`
    30  func PathJoinRel(elem ...string) string {
    31  	elems := make([]string, len(elem))
    32  	for i, e := range elem {
    33  		if e == "" {
    34  			continue
    35  		}
    36  		elems[i] = path.Clean("/" + e)
    37  	}
    38  	p := path.Join(elems...)
    39  	if p == "" {
    40  		return ""
    41  	} else if p == "/" {
    42  		return "."
    43  	}
    44  	return p[1:]
    45  }
    46  
    47  // PathJoinRelX joins the path elements into a single path like PathJoinRel,
    48  // and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
    49  // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
    50  // It returns similar results as PathJoinRel except:
    51  //
    52  //	`foo\..\bar` => `bar`  (because it's processed as `foo/../bar`)
    53  //
    54  // All backslashes are handled as slashes, the result only contains slashes.
    55  func PathJoinRelX(elem ...string) string {
    56  	elems := make([]string, len(elem))
    57  	for i, e := range elem {
    58  		if e == "" {
    59  			continue
    60  		}
    61  		elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
    62  	}
    63  	return PathJoinRel(elems...)
    64  }
    65  
    66  const pathSeparator = string(os.PathSeparator)
    67  
    68  // FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
    69  // All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
    70  // The first element must be an absolute path, caller should prepare the base path.
    71  // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
    72  // Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
    73  //
    74  //	{`/foo`, ``, `bar`} => `/foo/bar`
    75  //	{`/foo`, `..`, `bar`} => `/foo/bar`
    76  func FilePathJoinAbs(base string, sub ...string) string {
    77  	elems := make([]string, 1, len(sub)+1)
    78  
    79  	// POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
    80  	// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
    81  	if isOSWindows() {
    82  		elems[0] = filepath.Clean(base)
    83  	} else {
    84  		elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator))
    85  	}
    86  	if !filepath.IsAbs(elems[0]) {
    87  		// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
    88  		panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
    89  	}
    90  	for _, s := range sub {
    91  		if s == "" {
    92  			continue
    93  		}
    94  		if isOSWindows() {
    95  			elems = append(elems, filepath.Clean(pathSeparator+s))
    96  		} else {
    97  			elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator)))
    98  		}
    99  	}
   100  	// the elems[0] must be an absolute path, just join them together
   101  	return filepath.Join(elems...)
   102  }
   103  
   104  // IsDir returns true if given path is a directory,
   105  // or returns false when it's a file or does not exist.
   106  func IsDir(dir string) (bool, error) {
   107  	f, err := os.Stat(dir)
   108  	if err == nil {
   109  		return f.IsDir(), nil
   110  	}
   111  	if os.IsNotExist(err) {
   112  		return false, nil
   113  	}
   114  	return false, err
   115  }
   116  
   117  // IsFile returns true if given path is a file,
   118  // or returns false when it's a directory or does not exist.
   119  func IsFile(filePath string) (bool, error) {
   120  	f, err := os.Stat(filePath)
   121  	if err == nil {
   122  		return !f.IsDir(), nil
   123  	}
   124  	if os.IsNotExist(err) {
   125  		return false, nil
   126  	}
   127  	return false, err
   128  }
   129  
   130  // IsExist checks whether a file or directory exists.
   131  // It returns false when the file or directory does not exist.
   132  func IsExist(path string) (bool, error) {
   133  	_, err := os.Stat(path)
   134  	if err == nil || os.IsExist(err) {
   135  		return true, nil
   136  	}
   137  	if os.IsNotExist(err) {
   138  		return false, nil
   139  	}
   140  	return false, err
   141  }
   142  
   143  func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) {
   144  	dir, err := os.Open(dirPath)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	defer dir.Close()
   149  
   150  	fis, err := dir.Readdir(0)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	statList := make([]string, 0)
   156  	for _, fi := range fis {
   157  		if CommonSkip(fi.Name()) {
   158  			continue
   159  		}
   160  
   161  		relPath := path.Join(recPath, fi.Name())
   162  		curPath := path.Join(dirPath, fi.Name())
   163  		if fi.IsDir() {
   164  			if includeDir {
   165  				statList = append(statList, relPath+"/")
   166  			}
   167  			s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
   168  			if err != nil {
   169  				return nil, err
   170  			}
   171  			statList = append(statList, s...)
   172  		} else if !isDirOnly {
   173  			statList = append(statList, relPath)
   174  		} else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 {
   175  			link, err := os.Readlink(curPath)
   176  			if err != nil {
   177  				return nil, err
   178  			}
   179  
   180  			isDir, err := IsDir(link)
   181  			if err != nil {
   182  				return nil, err
   183  			}
   184  			if isDir {
   185  				if includeDir {
   186  					statList = append(statList, relPath+"/")
   187  				}
   188  				s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
   189  				if err != nil {
   190  					return nil, err
   191  				}
   192  				statList = append(statList, s...)
   193  			}
   194  		}
   195  	}
   196  	return statList, nil
   197  }
   198  
   199  // StatDir gathers information of given directory by depth-first.
   200  // It returns slice of file list and includes subdirectories if enabled;
   201  // it returns error and nil slice when error occurs in underlying functions,
   202  // or given path is not a directory or does not exist.
   203  //
   204  // Slice does not include given path itself.
   205  // If subdirectories is enabled, they will have suffix '/'.
   206  func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
   207  	if isDir, err := IsDir(rootPath); err != nil {
   208  		return nil, err
   209  	} else if !isDir {
   210  		return nil, errors.New("not a directory or does not exist: " + rootPath)
   211  	}
   212  
   213  	isIncludeDir := false
   214  	if len(includeDir) != 0 {
   215  		isIncludeDir = includeDir[0]
   216  	}
   217  	return statDir(rootPath, "", isIncludeDir, false, false)
   218  }
   219  
   220  func isOSWindows() bool {
   221  	return runtime.GOOS == "windows"
   222  }
   223  
   224  var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/")
   225  
   226  // FileURLToPath extracts the path information from a file://... url.
   227  // It returns an error only if the URL is not a file URL.
   228  func FileURLToPath(u *url.URL) (string, error) {
   229  	if u.Scheme != "file" {
   230  		return "", errors.New("URL scheme is not 'file': " + u.String())
   231  	}
   232  
   233  	path := u.Path
   234  
   235  	if !isOSWindows() {
   236  		return path, nil
   237  	}
   238  
   239  	// If it looks like there's a Windows drive letter at the beginning, strip off the leading slash.
   240  	if driveLetterRegexp.MatchString(path) {
   241  		return path[1:], nil
   242  	}
   243  	return path, nil
   244  }
   245  
   246  // HomeDir returns path of '~'(in Linux) on Windows,
   247  // it returns error when the variable does not exist.
   248  func HomeDir() (home string, err error) {
   249  	// TODO: some users run Gitea with mismatched uid  and "HOME=xxx" (they set HOME=xxx by environment manually)
   250  	// TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
   251  	// so at the moment we can not use `user.Current().HomeDir`
   252  	if isOSWindows() {
   253  		home = os.Getenv("USERPROFILE")
   254  		if home == "" {
   255  			home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
   256  		}
   257  	} else {
   258  		home = os.Getenv("HOME")
   259  	}
   260  
   261  	if home == "" {
   262  		return "", errors.New("cannot get home directory")
   263  	}
   264  
   265  	return home, nil
   266  }
   267  
   268  // CommonSkip will check a provided name to see if it represents file or directory that should not be watched
   269  func CommonSkip(name string) bool {
   270  	if name == "" {
   271  		return true
   272  	}
   273  
   274  	switch name[0] {
   275  	case '.':
   276  		return true
   277  	case 't', 'T':
   278  		return name[1:] == "humbs.db"
   279  	case 'd', 'D':
   280  		return name[1:] == "esktop.ini"
   281  	}
   282  
   283  	return false
   284  }
   285  
   286  // IsReadmeFileName reports whether name looks like a README file
   287  // based on its name.
   288  func IsReadmeFileName(name string) bool {
   289  	name = strings.ToLower(name)
   290  	if len(name) < 6 {
   291  		return false
   292  	} else if len(name) == 6 {
   293  		return name == "readme"
   294  	}
   295  	return name[:7] == "readme."
   296  }
   297  
   298  // IsReadmeFileExtension reports whether name looks like a README file
   299  // based on its name. It will look through the provided extensions and check if the file matches
   300  // one of the extensions and provide the index in the extension list.
   301  // If the filename is `readme.` with an unmatched extension it will match with the index equaling
   302  // the length of the provided extension list.
   303  // Note that the '.' should be provided in ext, e.g ".md"
   304  func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
   305  	name = strings.ToLower(name)
   306  	if len(name) < 6 || name[:6] != "readme" {
   307  		return 0, false
   308  	}
   309  
   310  	for i, extension := range ext {
   311  		extension = strings.ToLower(extension)
   312  		if name[6:] == extension {
   313  			return i, true
   314  		}
   315  	}
   316  
   317  	if name[6] == '.' {
   318  		return len(ext), true
   319  	}
   320  
   321  	return 0, false
   322  }