Skip to content

Commit ce9dee5

Browse files
authored
Introduce path Clean/Join helper functions (#23495)
Since #23493 has conflicts with latest commits, this PR is my proposal for fixing #23371 Details are in the comments And refactor the `modules/options` module, to make it always use "filepath" to access local files. Benefits: * No need to do `util.CleanPath(strings.ReplaceAll(p, "\\", "/"))), "/")` any more (not only one before) * The function behaviors are clearly defined
1 parent 253a00a commit ce9dee5

File tree

16 files changed

+261
-152
lines changed

16 files changed

+261
-152
lines changed

models/git/lfs_lock.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func init() {
3434

3535
// BeforeInsert is invoked from XORM before inserting an object of this type.
3636
func (l *LFSLock) BeforeInsert() {
37-
l.Path = util.CleanPath(l.Path)
37+
l.Path = util.PathJoinRel(l.Path)
3838
}
3939

4040
// CreateLFSLock creates a new lock.
@@ -49,7 +49,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo
4949
return nil, err
5050
}
5151

52-
lock.Path = util.CleanPath(lock.Path)
52+
lock.Path = util.PathJoinRel(lock.Path)
5353
lock.RepoID = repo.ID
5454

5555
l, err := GetLFSLock(dbCtx, repo, lock.Path)
@@ -69,7 +69,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo
6969

7070
// GetLFSLock returns release by given path.
7171
func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (*LFSLock, error) {
72-
path = util.CleanPath(path)
72+
path = util.PathJoinRel(path)
7373
rel := &LFSLock{RepoID: repo.ID}
7474
has, err := db.GetEngine(ctx).Where("lower(path) = ?", strings.ToLower(path)).Get(rel)
7575
if err != nil {

modules/options/base.go

+53-14
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,38 @@ import (
77
"fmt"
88
"io/fs"
99
"os"
10-
"path"
1110
"path/filepath"
1211

12+
"code.gitea.io/gitea/modules/log"
1313
"code.gitea.io/gitea/modules/setting"
1414
"code.gitea.io/gitea/modules/util"
1515
)
1616

17+
var directories = make(directorySet)
18+
1719
// Locale reads the content of a specific locale from static/bindata or custom path.
1820
func Locale(name string) ([]byte, error) {
19-
return fileFromDir(path.Join("locale", util.CleanPath(name)))
21+
return fileFromOptionsDir("locale", name)
2022
}
2123

2224
// Readme reads the content of a specific readme from static/bindata or custom path.
2325
func Readme(name string) ([]byte, error) {
24-
return fileFromDir(path.Join("readme", util.CleanPath(name)))
26+
return fileFromOptionsDir("readme", name)
2527
}
2628

2729
// Gitignore reads the content of a gitignore locale from static/bindata or custom path.
2830
func Gitignore(name string) ([]byte, error) {
29-
return fileFromDir(path.Join("gitignore", util.CleanPath(name)))
31+
return fileFromOptionsDir("gitignore", name)
3032
}
3133

3234
// License reads the content of a specific license from static/bindata or custom path.
3335
func License(name string) ([]byte, error) {
34-
return fileFromDir(path.Join("license", util.CleanPath(name)))
36+
return fileFromOptionsDir("license", name)
3537
}
3638

3739
// Labels reads the content of a specific labels from static/bindata or custom path.
3840
func Labels(name string) ([]byte, error) {
39-
return fileFromDir(path.Join("label", util.CleanPath(name)))
41+
return fileFromOptionsDir("label", name)
4042
}
4143

4244
// WalkLocales reads the content of a specific locale
@@ -79,17 +81,54 @@ func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, e
7981
return nil
8082
}
8183

82-
func statDirIfExist(dir string) ([]string, error) {
83-
isDir, err := util.IsDir(dir)
84+
// mustLocalPathAbs coverts a path to absolute path
85+
// FIXME: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
86+
func mustLocalPathAbs(s string) string {
87+
abs, err := filepath.Abs(s)
8488
if err != nil {
85-
return nil, fmt.Errorf("unable to check if static directory %s is a directory. %w", dir, err)
89+
// This should never happen in a real system. If it happens, the user must have already been in trouble: the system is not able to resolve its own paths.
90+
log.Fatal("Unable to get absolute path for %q: %v", s, err)
8691
}
87-
if !isDir {
88-
return nil, nil
92+
return abs
93+
}
94+
95+
func joinLocalPaths(baseDirs []string, subDir string, elems ...string) (paths []string) {
96+
abs := make([]string, len(elems)+2)
97+
abs[1] = subDir
98+
copy(abs[2:], elems)
99+
for _, baseDir := range baseDirs {
100+
abs[0] = mustLocalPathAbs(baseDir)
101+
paths = append(paths, util.FilePathJoinAbs(abs...))
89102
}
90-
files, err := util.StatDir(dir, true)
91-
if err != nil {
92-
return nil, fmt.Errorf("unable to read directory %q. %w", dir, err)
103+
return paths
104+
}
105+
106+
func listLocalDirIfExist(baseDirs []string, subDir string, elems ...string) (files []string, err error) {
107+
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
108+
isDir, err := util.IsDir(localPath)
109+
if err != nil {
110+
return nil, fmt.Errorf("unable to check if path %q is a directory. %w", localPath, err)
111+
} else if !isDir {
112+
continue
113+
}
114+
115+
dirFiles, err := util.StatDir(localPath, true)
116+
if err != nil {
117+
return nil, fmt.Errorf("unable to read directory %q. %w", localPath, err)
118+
}
119+
files = append(files, dirFiles...)
93120
}
94121
return files, nil
95122
}
123+
124+
func readLocalFile(baseDirs []string, subDir string, elems ...string) ([]byte, error) {
125+
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
126+
data, err := os.ReadFile(localPath)
127+
if err == nil {
128+
return data, nil
129+
} else if !os.IsNotExist(err) {
130+
log.Error("Unable to read file %q. Error: %v", localPath, err)
131+
}
132+
}
133+
return nil, os.ErrNotExist
134+
}

modules/options/dynamic.go

+6-42
Original file line numberDiff line numberDiff line change
@@ -6,62 +6,26 @@
66
package options
77

88
import (
9-
"fmt"
10-
"os"
11-
"path"
12-
13-
"code.gitea.io/gitea/modules/log"
149
"code.gitea.io/gitea/modules/setting"
15-
"code.gitea.io/gitea/modules/util"
1610
)
1711

18-
var directories = make(directorySet)
19-
2012
// Dir returns all files from static or custom directory.
2113
func Dir(name string) ([]string, error) {
2214
if directories.Filled(name) {
2315
return directories.Get(name), nil
2416
}
2517

26-
var result []string
27-
28-
for _, dir := range []string{
29-
path.Join(setting.CustomPath, "options", name), // custom dir
30-
path.Join(setting.StaticRootPath, "options", name), // static dir
31-
} {
32-
files, err := statDirIfExist(dir)
33-
if err != nil {
34-
return nil, err
35-
}
36-
result = append(result, files...)
18+
result, err := listLocalDirIfExist([]string{setting.CustomPath, setting.StaticRootPath}, "options", name)
19+
if err != nil {
20+
return nil, err
3721
}
3822

3923
return directories.AddAndGet(name, result), nil
4024
}
4125

42-
// fileFromDir is a helper to read files from static or custom path.
43-
func fileFromDir(name string) ([]byte, error) {
44-
customPath := path.Join(setting.CustomPath, "options", name)
45-
46-
isFile, err := util.IsFile(customPath)
47-
if err != nil {
48-
log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
49-
}
50-
if isFile {
51-
return os.ReadFile(customPath)
52-
}
53-
54-
staticPath := path.Join(setting.StaticRootPath, "options", name)
55-
56-
isFile, err = util.IsFile(staticPath)
57-
if err != nil {
58-
log.Error("Unable to check if %s is a file. Error: %v", staticPath, err)
59-
}
60-
if isFile {
61-
return os.ReadFile(staticPath)
62-
}
63-
64-
return []byte{}, fmt.Errorf("Asset file does not exist: %s", name)
26+
// fileFromOptionsDir is a helper to read files from custom or static path.
27+
func fileFromOptionsDir(elems ...string) ([]byte, error) {
28+
return readLocalFile([]string{setting.CustomPath, setting.StaticRootPath}, "options", elems...)
6529
}
6630

6731
// IsDynamic will return false when using embedded data (-tags bindata)

modules/options/static.go

+10-29
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,20 @@ package options
88
import (
99
"fmt"
1010
"io"
11-
"os"
12-
"path"
1311

14-
"code.gitea.io/gitea/modules/log"
1512
"code.gitea.io/gitea/modules/setting"
1613
"code.gitea.io/gitea/modules/util"
1714
)
1815

19-
var directories = make(directorySet)
20-
21-
// Dir returns all files from bindata or custom directory.
16+
// Dir returns all files from custom directory or bindata.
2217
func Dir(name string) ([]string, error) {
2318
if directories.Filled(name) {
2419
return directories.Get(name), nil
2520
}
2621

27-
var result []string
28-
29-
for _, dir := range []string{
30-
path.Join(setting.CustomPath, "options", name), // custom dir
31-
// no static dir
32-
} {
33-
files, err := statDirIfExist(dir)
34-
if err != nil {
35-
return nil, err
36-
}
37-
result = append(result, files...)
22+
result, err := listLocalDirIfExist([]string{setting.CustomPath}, "options", name)
23+
if err != nil {
24+
return nil, err
3825
}
3926

4027
files, err := AssetDir(name)
@@ -64,24 +51,18 @@ func AssetDir(dirName string) ([]string, error) {
6451
return results, nil
6552
}
6653

67-
// fileFromDir is a helper to read files from bindata or custom path.
68-
func fileFromDir(name string) ([]byte, error) {
69-
customPath := path.Join(setting.CustomPath, "options", name)
70-
71-
isFile, err := util.IsFile(customPath)
72-
if err != nil {
73-
log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
74-
}
75-
if isFile {
76-
return os.ReadFile(customPath)
54+
// fileFromOptionsDir is a helper to read files from custom path or bindata.
55+
func fileFromOptionsDir(elems ...string) ([]byte, error) {
56+
// only try custom dir, no static dir
57+
if data, err := readLocalFile([]string{setting.CustomPath}, "options", elems...); err == nil {
58+
return data, nil
7759
}
7860

79-
f, err := Assets.Open(name)
61+
f, err := Assets.Open(util.PathJoinRelX(elems...))
8062
if err != nil {
8163
return nil, err
8264
}
8365
defer f.Close()
84-
8566
return io.ReadAll(f)
8667
}
8768

modules/public/public.go

+8-18
Original file line numberDiff line numberDiff line change
@@ -45,29 +45,19 @@ func AssetsHandlerFunc(opts *Options) http.HandlerFunc {
4545
return
4646
}
4747

48-
file := req.URL.Path
49-
file = file[len(opts.Prefix):]
50-
if len(file) == 0 {
51-
resp.WriteHeader(http.StatusNotFound)
52-
return
53-
}
54-
if strings.Contains(file, "\\") {
55-
resp.WriteHeader(http.StatusBadRequest)
56-
return
57-
}
58-
file = "/" + file
59-
60-
var written bool
48+
var corsSent bool
6149
if opts.CorsHandler != nil {
62-
written = true
6350
opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
64-
written = false
51+
corsSent = true
6552
})).ServeHTTP(resp, req)
6653
}
67-
if written {
54+
// If CORS is not sent, the response must have been written by other handlers
55+
if !corsSent {
6856
return
6957
}
7058

59+
file := req.URL.Path[len(opts.Prefix):]
60+
7161
// custom files
7262
if opts.handle(resp, req, http.Dir(custPath), file) {
7363
return
@@ -102,8 +92,8 @@ func setWellKnownContentType(w http.ResponseWriter, file string) {
10292
}
10393

10494
func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
105-
// use clean to keep the file is a valid path with no . or ..
106-
f, err := fs.Open(util.CleanPath(file))
95+
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
96+
f, err := fs.Open(util.PathJoinRelX(file))
10797
if err != nil {
10898
if os.IsNotExist(err) {
10999
return false

modules/storage/local.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ package storage
55

66
import (
77
"context"
8+
"fmt"
89
"io"
910
"net/url"
1011
"os"
1112
"path/filepath"
12-
"strings"
1313

1414
"code.gitea.io/gitea/modules/log"
1515
"code.gitea.io/gitea/modules/util"
@@ -41,13 +41,19 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
4141
}
4242
config := configInterface.(LocalStorageConfig)
4343

44+
if !filepath.IsAbs(config.Path) {
45+
return nil, fmt.Errorf("LocalStorageConfig.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path)
46+
}
4447
log.Info("Creating new Local Storage at %s", config.Path)
4548
if err := os.MkdirAll(config.Path, os.ModePerm); err != nil {
4649
return nil, err
4750
}
4851

4952
if config.TemporaryPath == "" {
50-
config.TemporaryPath = config.Path + "/tmp"
53+
config.TemporaryPath = filepath.Join(config.Path, "tmp")
54+
}
55+
if !filepath.IsAbs(config.TemporaryPath) {
56+
return nil, fmt.Errorf("LocalStorageConfig.TemporaryPath should be an absolute path, but not: %q", config.TemporaryPath)
5157
}
5258

5359
return &LocalStorage{
@@ -58,7 +64,7 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
5864
}
5965

6066
func (l *LocalStorage) buildLocalPath(p string) string {
61-
return filepath.Join(l.dir, util.CleanPath(strings.ReplaceAll(p, "\\", "/")))
67+
return util.FilePathJoinAbs(l.dir, p)
6268
}
6369

6470
// Open a file
@@ -128,10 +134,7 @@ func (l *LocalStorage) URL(path, name string) (*url.URL, error) {
128134

129135
// IterateObjects iterates across the objects in the local storage
130136
func (l *LocalStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error {
131-
dir := l.dir
132-
if prefix != "" {
133-
dir = filepath.Join(l.dir, util.CleanPath(prefix))
134-
}
137+
dir := l.buildLocalPath(prefix)
135138
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
136139
if err != nil {
137140
return err

0 commit comments

Comments
 (0)