Files
silverwind 1031e34987 test: run integration tests on random ports and speed up slow tests (#39842)
Integration test processes now get their own work directory, config file
and random ports, so several runs can execute in parallel on one
machine. The slowest tests also lose fixed sleeps and needlessly
expensive setup, with their assertions kept.

- Batch PostgreSQL sequence resets, the main per-test fixture cost there
- Cheap hashing params in hash and OAuth2 fixture tests, full-cost
vectors stay
- Single fetch for negative Actions runner assertions instead of 1s
polls
- Queue flushes and backdated timestamps instead of sleeps
- In-memory SQLite test journal, tmpfs for PostgreSQL in CI
- E2E instance writes SSH keys into its work directory instead of
`~/.ssh/authorized_keys`
2026-10-18 12:56:04 +00:00

369 lines
11 KiB
Go

// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"gitea.dev/modules/consts"
)
var PathNameValidator = sync.OnceValue(func() (ret struct {
InvalidChars *regexp.Regexp
InvalidNames *regexp.Regexp
},
) {
ret.InvalidChars = regexp.MustCompile(`(?i)[<>:"/\\|?*\x{0000}-\x{001F}]`)
// invalid filename contents, based on https://github.com/sindresorhus/filename-reserved-regex
// "COM10" needs to be opened with UNC "\\.\COM10" on Windows, so itself is valid
ret.InvalidNames = regexp.MustCompile(`(?i)^(con|prn|aux|nul|com\d|lpt\d)$`)
return ret
})
func PathBaseStem(s string) string {
return strings.TrimSuffix(path.Base(s), path.Ext(s))
}
// PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately.
// It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed.
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
//
// empty => ``
// `` => ``
// `..` => `.`
// `dir` => `dir`
// `/dir/` => `dir`
// `foo\..\bar` => `foo\..\bar`
// {`foo`, ``, `bar`} => `foo/bar`
// {`foo`, `..`, `bar`} => `foo/bar`
func PathJoinRel(elem ...string) string {
elems := make([]string, len(elem))
for i, e := range elem {
if e == "" {
continue
}
elems[i] = path.Clean("/" + e)
}
p := path.Join(elems...)
switch p {
case "":
return ""
case "/":
return "."
}
return p[1:]
}
// PathJoinRelX joins the path elements into a single path like PathJoinRel,
// and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
// It returns similar results as PathJoinRel except:
//
// `foo\..\bar` => `bar` (because it's processed as `foo/../bar`)
//
// All backslashes are handled as slashes, the result only contains slashes.
func PathJoinRelX(elem ...string) string {
elems := make([]string, len(elem))
for i, e := range elem {
if e == "" {
continue
}
elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
}
return PathJoinRel(elems...)
}
const filepathSeparator = string(os.PathSeparator)
// FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
// All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
// The first element must be an absolute path, caller should prepare the base path.
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
// Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
//
// {`/foo`, ``, `bar`} => `/foo/bar`
// {`/foo`, `..`, `bar`} => `/foo/bar`
func FilePathJoinAbs(base string, sub ...string) string {
// POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
if !consts.IsWindows {
base = strings.ReplaceAll(base, "\\", filepathSeparator)
}
if !filepath.IsAbs(base) {
// This shouldn't happen. If it is really necessary to handle relative paths, use filepath.Abs() to get absolute paths first
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", base, sub))
}
if len(sub) == 0 {
return filepath.Clean(base)
}
elems := make([]string, 1, len(sub)+1)
elems[0] = base
for _, s := range sub {
if s == "" {
continue
}
if consts.IsWindows {
elems = append(elems, filepath.Clean(filepathSeparator+s))
} else {
elems = append(elems, filepath.Clean(filepathSeparator+strings.ReplaceAll(s, "\\", filepathSeparator)))
}
}
// the elems[0] must be an absolute path, just join them together, and Join will also do Clean
return filepath.Join(elems...)
}
// IsDir returns true if given path is a directory,
// or returns false when it's a file or does not exist.
func IsDir(dir string) (bool, error) {
f, err := os.Stat(dir)
if err == nil {
return f.IsDir(), nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
var ErrNotRegularPathFile = errors.New("not a regular file")
// ReadRegularPathFile reads a file with given sub path in root dir.
// It returns error when the path is not a regular file, or any parent path is not a regular directory.
func ReadRegularPathFile(root, filePathIn string, limit int) ([]byte, error) {
pathFields := strings.Split(PathJoinRelX(filePathIn), "/")
targetPathBuilder := strings.Builder{}
targetPathBuilder.Grow(len(root) + len(filePathIn) + 2)
targetPathBuilder.WriteString(root)
targetPathString := root
for i, subPath := range pathFields {
targetPathBuilder.WriteByte(filepath.Separator)
targetPathBuilder.WriteString(subPath)
targetPathString = targetPathBuilder.String()
expectFile := i == len(pathFields)-1
st, err := os.Lstat(targetPathString)
if err != nil {
return nil, err
}
if expectFile && !st.Mode().IsRegular() || !expectFile && !st.Mode().IsDir() {
return nil, fmt.Errorf("%w: %s", ErrNotRegularPathFile, filePathIn)
}
}
f, err := os.Open(targetPathString)
if err != nil {
return nil, err
}
defer f.Close()
return ReadWithLimit(f, limit)
}
// WriteRegularPathFile writes data to a file with given sub path in root dir, it creates parent directories if necessary.
// The file is created with fileMode, and the directories are created with dirMode.
// It returns error when the path already exists but is not a regular file, or any parent path is not a regular directory.
func WriteRegularPathFile(root, filePathIn string, data []byte, dirMode, fileMode os.FileMode) error {
pathFields := strings.Split(PathJoinRelX(filePathIn), "/")
targetPathBuilder := strings.Builder{}
targetPathBuilder.Grow(len(root) + len(filePathIn) + 2)
targetPathBuilder.WriteString(root)
targetPathString := root
for i, subPath := range pathFields {
targetPathBuilder.WriteByte(filepath.Separator)
targetPathBuilder.WriteString(subPath)
targetPathString = targetPathBuilder.String()
expectFile := i == len(pathFields)-1
st, err := os.Lstat(targetPathString)
if err == nil {
if expectFile && !st.Mode().IsRegular() || !expectFile && !st.Mode().IsDir() {
return fmt.Errorf("%w: %s", ErrNotRegularPathFile, filePathIn)
}
continue
}
if !os.IsNotExist(err) {
return err
}
if !expectFile {
if err = os.Mkdir(targetPathString, dirMode); err != nil {
return err
}
}
}
return os.WriteFile(targetPathString, data, fileMode)
}
// IsExist checks whether a file or directory exists.
// It returns false when the file or directory does not exist.
func IsExist(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil || os.IsExist(err) {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error {
dir, err := os.Open(fsDir)
if err != nil {
return err
}
defer dir.Close()
fis, err := dir.ReadDir(0)
if err != nil {
return err
}
for _, fi := range fis {
if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) {
continue
}
relPath := path.Join(recordParentPath, fi.Name())
curPath := filepath.Join(fsDir, fi.Name())
if fi.IsDir() {
if opts.IncludeDir {
*result = append(*result, relPath+"/")
}
if err = listDirRecursively(result, curPath, relPath, opts); err != nil {
return err
}
} else {
*result = append(*result, relPath)
}
}
return nil
}
type ListDirOptions struct {
IncludeDir bool // subdirectories are also included with suffix slash
SkipCommonHiddenNames bool
}
// ListDirRecursively gathers information of given directory by depth-first.
// The paths are always in "dir/slash/file" format (not "\\" even in Windows)
// Slice does not include given path itself.
func ListDirRecursively(rootDir string, opts *ListDirOptions) (res []string, err error) {
if err = listDirRecursively(&res, rootDir, "", opts); err != nil {
return nil, err
}
return res, nil
}
func fileURLToPathInternal(u *url.URL, isWindows bool) (string, error) {
if u.Scheme != "file" {
return "", errors.New("URL scheme is not 'file': " + u.String())
}
if !isWindows {
return u.Path, nil
}
// If it is a Windows absolute path with drive letter "/C:/dir", strip off the leading slash.
if !strings.HasPrefix(u.Path, "/") || len(u.Path) < 3 {
return u.Path, nil
}
winPath := u.Path[1:]
first := winPath[0]
if ('a' <= first && first <= 'z' || 'A' <= first && first <= 'Z') && winPath[1] == ':' {
return winPath, nil
}
return u.Path, nil
}
// FileURLToPath extracts the path information from a file://... url.
// It returns an error only if the URL is not a file URL.
func FileURLToPath(u *url.URL) (string, error) {
return fileURLToPathInternal(u, consts.IsWindows)
}
// HomeDir returns path of '~'(in Linux) on Windows,
// it returns error when the variable does not exist.
func HomeDir() (home string, err error) {
// TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually)
// TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
// so at the moment we can not use `user.Current().HomeDir`
if consts.IsWindows {
home = os.Getenv("USERPROFILE")
if home == "" {
home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
} else {
home = os.Getenv("HOME")
}
if home == "" {
return "", errors.New("cannot get home directory")
}
return home, nil
}
// IsCommonHiddenFileName will check a provided name to see if it represents file or directory that should not be watched
func IsCommonHiddenFileName(name string) bool {
if name == "" {
return true
}
switch name[0] {
case '.':
return true
case 't', 'T':
return name[1:] == "humbs.db" // macOS
case 'd', 'D':
return name[1:] == "esktop.ini" // Windows
}
return false
}
// IsReadmeFileName reports whether name looks like a README file
// based on its name.
func IsReadmeFileName(name string) bool {
name = strings.ToLower(name)
if len(name) < 6 {
return false
} else if len(name) == 6 {
return name == "readme"
}
return name[:7] == "readme."
}
// IsReadmeFileExtension reports whether name looks like a README file
// based on its name. It will look through the provided extensions and check if the file matches
// one of the extensions and provide the index in the extension list.
// If the filename is `readme.` with an unmatched extension it will match with the index equaling
// the length of the provided extension list.
// Note that the '.' should be provided in ext, e.g ".md"
func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
name = strings.ToLower(name)
if len(name) < 6 || name[:6] != "readme" {
return 0, false
}
for i, extension := range ext {
extension = strings.ToLower(extension)
if name[6:] == extension {
return i, true
}
}
if name[6] == '.' {
return len(ext), true
}
return 0, false
}