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