code.gitea.io/gitea@v1.19.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  	} else {
    44  		return p[1:]
    45  	}
    46  }
    47  
    48  // PathJoinRelX joins the path elements into a single path like PathJoinRel,
    49  // and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
    50  // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
    51  // It returns similar results as PathJoinRel except:
    52  //
    53  //	`foo\..\bar` => `bar`  (because it's processed as `foo/../bar`)
    54  //
    55  // All backslashes are handled as slashes, the result only contains slashes.
    56  func PathJoinRelX(elem ...string) string {
    57  	elems := make([]string, len(elem))
    58  	for i, e := range elem {
    59  		if e == "" {
    60  			continue
    61  		}
    62  		elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
    63  	}
    64  	return PathJoinRel(elems...)
    65  }
    66  
    67  const pathSeparator = string(os.PathSeparator)
    68  
    69  // FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
    70  // All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
    71  // The first element must be an absolute path, caller should prepare the base path.
    72  // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
    73  // Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
    74  //
    75  //	{`/foo`, ``, `bar`} => `/foo/bar`
    76  //	{`/foo`, `..`, `bar`} => `/foo/bar`
    77  func FilePathJoinAbs(elem ...string) string {
    78  	elems := make([]string, len(elem))
    79  
    80  	// POISX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
    81  	// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
    82  	if isOSWindows() {
    83  		elems[0] = filepath.Clean(elem[0])
    84  	} else {
    85  		elems[0] = filepath.Clean(strings.ReplaceAll(elem[0], "\\", pathSeparator))
    86  	}
    87  	if !filepath.IsAbs(elems[0]) {
    88  		// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
    89  		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))
    90  	}
    91  
    92  	for i := 1; i < len(elem); i++ {
    93  		if elem[i] == "" {
    94  			continue
    95  		}
    96  		if isOSWindows() {
    97  			elems[i] = filepath.Clean(pathSeparator + elem[i])
    98  		} else {
    99  			elems[i] = filepath.Clean(pathSeparator + strings.ReplaceAll(elem[i], "\\", pathSeparator))
   100  		}
   101  	}
   102  	// the elems[0] must be an absolute path, just join them together
   103  	return filepath.Join(elems...)
   104  }
   105  
   106  // IsDir returns true if given path is a directory,
   107  // or returns false when it's a file or does not exist.
   108  func IsDir(dir string) (bool, error) {
   109  	f, err := os.Stat(dir)
   110  	if err == nil {
   111  		return f.IsDir(), nil
   112  	}
   113  	if os.IsNotExist(err) {
   114  		return false, nil
   115  	}
   116  	return false, err
   117  }
   118  
   119  // IsFile returns true if given path is a file,
   120  // or returns false when it's a directory or does not exist.
   121  func IsFile(filePath string) (bool, error) {
   122  	f, err := os.Stat(filePath)
   123  	if err == nil {
   124  		return !f.IsDir(), nil
   125  	}
   126  	if os.IsNotExist(err) {
   127  		return false, nil
   128  	}
   129  	return false, err
   130  }
   131  
   132  // IsExist checks whether a file or directory exists.
   133  // It returns false when the file or directory does not exist.
   134  func IsExist(path string) (bool, error) {
   135  	_, err := os.Stat(path)
   136  	if err == nil || os.IsExist(err) {
   137  		return true, nil
   138  	}
   139  	if os.IsNotExist(err) {
   140  		return false, nil
   141  	}
   142  	return false, err
   143  }
   144  
   145  func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) {
   146  	dir, err := os.Open(dirPath)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	defer dir.Close()
   151  
   152  	fis, err := dir.Readdir(0)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	statList := make([]string, 0)
   158  	for _, fi := range fis {
   159  		if CommonSkip(fi.Name()) {
   160  			continue
   161  		}
   162  
   163  		relPath := path.Join(recPath, fi.Name())
   164  		curPath := path.Join(dirPath, fi.Name())
   165  		if fi.IsDir() {
   166  			if includeDir {
   167  				statList = append(statList, relPath+"/")
   168  			}
   169  			s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
   170  			if err != nil {
   171  				return nil, err
   172  			}
   173  			statList = append(statList, s...)
   174  		} else if !isDirOnly {
   175  			statList = append(statList, relPath)
   176  		} else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 {
   177  			link, err := os.Readlink(curPath)
   178  			if err != nil {
   179  				return nil, err
   180  			}
   181  
   182  			isDir, err := IsDir(link)
   183  			if err != nil {
   184  				return nil, err
   185  			}
   186  			if isDir {
   187  				if includeDir {
   188  					statList = append(statList, relPath+"/")
   189  				}
   190  				s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
   191  				if err != nil {
   192  					return nil, err
   193  				}
   194  				statList = append(statList, s...)
   195  			}
   196  		}
   197  	}
   198  	return statList, nil
   199  }
   200  
   201  // StatDir gathers information of given directory by depth-first.
   202  // It returns slice of file list and includes subdirectories if enabled;
   203  // it returns error and nil slice when error occurs in underlying functions,
   204  // or given path is not a directory or does not exist.
   205  //
   206  // Slice does not include given path itself.
   207  // If subdirectories is enabled, they will have suffix '/'.
   208  func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
   209  	if isDir, err := IsDir(rootPath); err != nil {
   210  		return nil, err
   211  	} else if !isDir {
   212  		return nil, errors.New("not a directory or does not exist: " + rootPath)
   213  	}
   214  
   215  	isIncludeDir := false
   216  	if len(includeDir) != 0 {
   217  		isIncludeDir = includeDir[0]
   218  	}
   219  	return statDir(rootPath, "", isIncludeDir, false, false)
   220  }
   221  
   222  func isOSWindows() bool {
   223  	return runtime.GOOS == "windows"
   224  }
   225  
   226  // FileURLToPath extracts the path information from a file://... url.
   227  func FileURLToPath(u *url.URL) (string, error) {
   228  	if u.Scheme != "file" {
   229  		return "", errors.New("URL scheme is not 'file': " + u.String())
   230  	}
   231  
   232  	path := u.Path
   233  
   234  	if !isOSWindows() {
   235  		return path, nil
   236  	}
   237  
   238  	// If it looks like there's a Windows drive letter at the beginning, strip off the leading slash.
   239  	re := regexp.MustCompile("/[A-Za-z]:/")
   240  	if re.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  }