feat: vendor gitea 1.16.2
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import "bytes"
|
||||
|
||||
func BufioScannerSplit(b byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
// reference: bufio.ScanLines
|
||||
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
if i := bytes.IndexByte(data, b); i >= 0 {
|
||||
return i + 1, data[0:i], nil
|
||||
}
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,14 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
|
||||
if len(hexString) == 8 {
|
||||
hexString = hexString[0:6]
|
||||
}
|
||||
color, err := strconv.ParseUint(hexString, 16, 64)
|
||||
color, err := strconv.ParseUint(hexString, 16, 32)
|
||||
color32 := uint32(color)
|
||||
if err != nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
r := float64(uint8(0xFF & (uint32(color) >> 16)))
|
||||
g := float64(uint8(0xFF & (uint32(color) >> 8)))
|
||||
b := float64(uint8(0xFF & uint32(color)))
|
||||
r := float64(uint8(0xFF & (color32 >> 16)))
|
||||
g := float64(uint8(0xFF & (color32 >> 8)))
|
||||
b := float64(uint8(0xFF & color32))
|
||||
return r, g, b
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ func PathJoinRelX(elem ...string) string {
|
||||
return PathJoinRel(elems...)
|
||||
}
|
||||
|
||||
const pathSeparator = string(os.PathSeparator)
|
||||
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.
|
||||
@@ -75,30 +75,32 @@ const pathSeparator = string(os.PathSeparator)
|
||||
// {`/foo`, ``, `bar`} => `/foo/bar`
|
||||
// {`/foo`, `..`, `bar`} => `/foo/bar`
|
||||
func FilePathJoinAbs(base string, sub ...string) string {
|
||||
elems := make([]string, 1, len(sub)+1)
|
||||
|
||||
// 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 isOSWindows() {
|
||||
elems[0] = filepath.Clean(base)
|
||||
} else {
|
||||
elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator))
|
||||
if !isOSWindows() {
|
||||
base = strings.ReplaceAll(base, "\\", filepathSeparator)
|
||||
}
|
||||
if !filepath.IsAbs(elems[0]) {
|
||||
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
|
||||
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))
|
||||
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 isOSWindows() {
|
||||
elems = append(elems, filepath.Clean(pathSeparator+s))
|
||||
elems = append(elems, filepath.Clean(filepathSeparator+s))
|
||||
} else {
|
||||
elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator)))
|
||||
elems = append(elems, filepath.Clean(filepathSeparator+strings.ReplaceAll(s, "\\", filepathSeparator)))
|
||||
}
|
||||
}
|
||||
// the elems[0] must be an absolute path, just join them together
|
||||
// the elems[0] must be an absolute path, just join them together, and Join will also do Clean
|
||||
return filepath.Join(elems...)
|
||||
}
|
||||
|
||||
@@ -115,12 +117,72 @@ func IsDir(dir string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
func IsRegularFile(filePath string) (bool, error) {
|
||||
f, err := os.Lstat(filePath)
|
||||
if err == nil {
|
||||
return f.Mode().IsRegular(), nil
|
||||
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)
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
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.
|
||||
|
||||
@@ -6,6 +6,7 @@ package util
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
@@ -230,3 +231,70 @@ func TestListDirRecursively(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res)
|
||||
}
|
||||
|
||||
func TestReadWriteRegularPathFile(t *testing.T) {
|
||||
const readLimit = 10000
|
||||
tmpDir := t.TempDir()
|
||||
rootDir := tmpDir + "/root"
|
||||
_ = os.Mkdir(rootDir, 0o755)
|
||||
_ = os.WriteFile(tmpDir+"/other-file", []byte("other-content"), 0o755)
|
||||
_ = os.Mkdir(rootDir+"/real-dir", 0o755)
|
||||
_ = os.WriteFile(rootDir+"/real-dir/real-file", []byte("dummy-content"), 0o644)
|
||||
_ = os.Symlink(rootDir+"/real-dir", rootDir+"/link-dir")
|
||||
_ = os.Symlink(rootDir+"/real-dir/real-file", rootDir+"/real-dir/link-file")
|
||||
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
content, err := os.ReadFile(filepath.Join(rootDir, "../other-file"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "other-content", string(content))
|
||||
|
||||
content, err = ReadRegularPathFile(rootDir, "../other-file", readLimit)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
assert.Empty(t, string(content))
|
||||
|
||||
content, err = ReadRegularPathFile(rootDir, "real-dir/real-file", readLimit)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "dummy-content", string(content))
|
||||
|
||||
_, err = ReadRegularPathFile(rootDir, "link-dir/real-file", readLimit)
|
||||
require.ErrorIs(t, err, ErrNotRegularPathFile)
|
||||
_, err = ReadRegularPathFile(rootDir, "real-dir/link-file", readLimit)
|
||||
require.ErrorIs(t, err, ErrNotRegularPathFile)
|
||||
_, err = ReadRegularPathFile(rootDir, "link-dir/link-file", readLimit)
|
||||
require.ErrorIs(t, err, ErrNotRegularPathFile)
|
||||
})
|
||||
|
||||
t.Run("Write", func(t *testing.T) {
|
||||
assertFileContent := func(path, expected string) {
|
||||
data, err := os.ReadFile(path)
|
||||
if expected == "" {
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, string(data), "file content mismatch for %s", path)
|
||||
}
|
||||
|
||||
err := WriteRegularPathFile(rootDir, "new-dir/new-file", []byte("new-content"), 0o755, 0o644)
|
||||
require.NoError(t, err)
|
||||
assertFileContent(rootDir+"/new-dir/new-file", "new-content")
|
||||
|
||||
err = WriteRegularPathFile(rootDir, "link-dir/real-file", []byte("new-content"), 0o755, 0o644)
|
||||
require.ErrorIs(t, err, ErrNotRegularPathFile)
|
||||
err = WriteRegularPathFile(rootDir, "link-dir/link-file", []byte("new-content"), 0o755, 0o644)
|
||||
require.ErrorIs(t, err, ErrNotRegularPathFile)
|
||||
err = WriteRegularPathFile(rootDir, "link-dir/new-file", []byte("new-content"), 0o755, 0o644)
|
||||
require.ErrorIs(t, err, ErrNotRegularPathFile)
|
||||
err = WriteRegularPathFile(rootDir, "real-dir/link-file", []byte("new-content"), 0o755, 0o644)
|
||||
require.ErrorIs(t, err, ErrNotRegularPathFile)
|
||||
|
||||
err = WriteRegularPathFile(rootDir, "../other-file", []byte("new-content"), 0o755, 0o644)
|
||||
require.NoError(t, err)
|
||||
assertFileContent(rootDir+"/../other-file", "other-content")
|
||||
assertFileContent(rootDir+"/other-file", "new-content")
|
||||
|
||||
err = WriteRegularPathFile(rootDir, "real-dir/real-file", []byte("changed-content"), 0o755, 0o644)
|
||||
require.NoError(t, err)
|
||||
assertFileContent(rootDir+"/real-dir/real-file", "changed-content")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ package util
|
||||
|
||||
import "runtime"
|
||||
|
||||
func CallerFuncName(skip int) string {
|
||||
func CallerFuncName(optSkipParent ...int) string {
|
||||
pc := make([]uintptr, 1)
|
||||
runtime.Callers(skip+1, pc)
|
||||
skipParent := 0
|
||||
if len(optSkipParent) > 0 {
|
||||
skipParent = optSkipParent[0]
|
||||
}
|
||||
runtime.Callers(skipParent+1 /*this*/ +1 /*runtime*/, pc)
|
||||
funcName := runtime.FuncForPC(pc[0]).Name()
|
||||
return funcName
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCallerFuncName(t *testing.T) {
|
||||
s := CallerFuncName(1)
|
||||
s := CallerFuncName()
|
||||
assert.Equal(t, "code.gitea.io/gitea/modules/util.TestCallerFuncName", s)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func BenchmarkCallerFuncName(b *testing.B) {
|
||||
// It is almost as fast as fmt.Sprintf
|
||||
b.Run("caller", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
CallerFuncName(1)
|
||||
CallerFuncName()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type sanitizedError struct {
|
||||
@@ -25,48 +26,103 @@ func SanitizeErrorCredentialURLs(err error) error {
|
||||
return sanitizedError{err: err}
|
||||
}
|
||||
|
||||
const userPlaceholder = "sanitized-credential"
|
||||
|
||||
var schemeSep = []byte("://")
|
||||
|
||||
// SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://") for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com"
|
||||
const userInfoPlaceholder = "(masked)"
|
||||
|
||||
// SanitizeCredentialURLs remove all credentials in URLs for the input string:
|
||||
// * "https://userinfo@domain.com" => "https://***@domain.com"
|
||||
// * "user:pass@domain.com" => "***@domain.com"
|
||||
// "***" is a magic string internally used, doesn't guarantee to be anything.
|
||||
func SanitizeCredentialURLs(s string) string {
|
||||
sepColPos := strings.Index(s, ":")
|
||||
if sepColPos == -1 {
|
||||
return s // fast path: no colon, unlikely contain any URL credential
|
||||
}
|
||||
sepAtPos := strings.Index(s[sepColPos+1:], "@")
|
||||
for sepAtPos == -1 {
|
||||
return s // fast path: no "@" after colon, unlikely contain any URL credential
|
||||
}
|
||||
sepAtPos += sepColPos + 1
|
||||
|
||||
res := make([]byte, 0, len(s)+len(userInfoPlaceholder)) // a best guess to avoid too many re-allocations
|
||||
bs := UnsafeStringToBytes(s)
|
||||
schemeSepPos := bytes.Index(bs, schemeSep)
|
||||
if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 {
|
||||
return s // fast return if there is no URL scheme or no userinfo
|
||||
}
|
||||
out := make([]byte, 0, len(bs)+len(userPlaceholder))
|
||||
for schemeSepPos != -1 {
|
||||
schemeSepPos += 3 // skip the "://"
|
||||
sepAtPos := -1 // the possible '@' position: "https://foo@[^here]host"
|
||||
sepEndPos := schemeSepPos // the possible end position: "The https://host[^here] in log for test"
|
||||
sepLoop:
|
||||
for ; sepEndPos < len(bs); sepEndPos++ {
|
||||
c := bs[sepEndPos]
|
||||
if ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') {
|
||||
continue
|
||||
}
|
||||
for {
|
||||
// left part (before "@") is likely to be the "userinfo" (single username, or "username:password")
|
||||
leftPos := sepAtPos - 1
|
||||
leftLoop:
|
||||
for leftPos >= 0 {
|
||||
c := bs[leftPos]
|
||||
switch c {
|
||||
case '@':
|
||||
sepAtPos = sepEndPos
|
||||
case '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '%':
|
||||
continue // due to RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars
|
||||
// RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars
|
||||
default:
|
||||
break sepLoop // if it is an invalid char for URL (eg: space, '/', and others), stop the loop
|
||||
valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9'
|
||||
if !valid {
|
||||
break leftLoop
|
||||
}
|
||||
}
|
||||
leftPos--
|
||||
}
|
||||
// if there is '@', and the string is like "s://u@h", then hide the "u" part
|
||||
if sepAtPos != -1 && (schemeSepPos >= 4 && unicode.IsLetter(rune(bs[schemeSepPos-4]))) && sepAtPos-schemeSepPos > 0 && sepEndPos-sepAtPos > 0 {
|
||||
out = append(out, bs[:schemeSepPos]...)
|
||||
out = append(out, userPlaceholder...)
|
||||
out = append(out, bs[sepAtPos:sepEndPos]...)
|
||||
// left pos should point to the beginning of the left part, this pos is always valid in the buffer
|
||||
leftPos++
|
||||
|
||||
// right part is likely to be the host (domain name, ip address)
|
||||
rightPos := sepAtPos + 1
|
||||
rightLoop:
|
||||
for rightPos < len(bs) {
|
||||
c := bs[rightPos]
|
||||
switch c {
|
||||
case '.', '-':
|
||||
// valid host char
|
||||
case '[':
|
||||
// ipv6 begin
|
||||
if rightPos != sepAtPos+1 {
|
||||
break rightLoop
|
||||
}
|
||||
case ']':
|
||||
// ipv6 end
|
||||
rightPos++
|
||||
break rightLoop
|
||||
default:
|
||||
valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9'
|
||||
if bs[sepAtPos+1] == '[' {
|
||||
// ipv6 host
|
||||
valid = 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' || '0' <= c && c <= '9' || c == ':'
|
||||
}
|
||||
if !valid {
|
||||
break rightLoop
|
||||
}
|
||||
}
|
||||
rightPos++
|
||||
}
|
||||
|
||||
leading, leftPart, rightPart := bs[:leftPos], bs[leftPos:sepAtPos], bs[sepAtPos+1:rightPos]
|
||||
|
||||
// Either:
|
||||
// * git log message: "user:pass@host" (it contains a colon in userinfo), ignore "git@host" pattern
|
||||
// * http like URL: "https://userinfo@host.com" (it has "://" before the userinfo)
|
||||
needSanitize := bytes.IndexByte(leftPart, ':') >= 0 || bytes.HasSuffix(leading, schemeSep)
|
||||
needSanitize = needSanitize && len(leftPart) > 0 && len(rightPart) > 0
|
||||
// TODO: can also do more checks for right part
|
||||
// for example: ipv6 quick check
|
||||
if needSanitize && rightPart[0] == '[' {
|
||||
needSanitize = rightPart[len(rightPart)-1] == ']' && net.ParseIP(UnsafeBytesToString(rightPart[1:len(rightPart)-1])) != nil
|
||||
}
|
||||
if needSanitize {
|
||||
res = append(res, leading...)
|
||||
res = append(res, userInfoPlaceholder...)
|
||||
res = append(res, '@')
|
||||
res = append(res, rightPart...)
|
||||
} else {
|
||||
out = append(out, bs[:sepEndPos]...)
|
||||
res = append(res, bs[:rightPos]...)
|
||||
}
|
||||
bs = bs[rightPos:]
|
||||
sepAtPos = bytes.IndexByte(bs, '@')
|
||||
if sepAtPos == -1 {
|
||||
break
|
||||
}
|
||||
bs = bs[sepEndPos:]
|
||||
schemeSepPos = bytes.Index(bs, schemeSep)
|
||||
}
|
||||
out = append(out, bs...)
|
||||
return UnsafeBytesToString(out)
|
||||
res = append(res, bs...)
|
||||
return UnsafeBytesToString(res)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func TestSanitizeErrorCredentialURLs(t *testing.T) {
|
||||
err := errors.New("error with https://a@b.com")
|
||||
se := SanitizeErrorCredentialURLs(err)
|
||||
assert.Equal(t, "error with https://"+userPlaceholder+"@b.com", se.Error())
|
||||
assert.Equal(t, "error with https://"+userInfoPlaceholder+"@b.com", se.Error())
|
||||
}
|
||||
|
||||
func TestSanitizeCredentialURLs(t *testing.T) {
|
||||
@@ -27,15 +27,35 @@ func TestSanitizeCredentialURLs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"https://mytoken@github.com/go-gitea/test_repo.git",
|
||||
"https://" + userPlaceholder + "@github.com/go-gitea/test_repo.git",
|
||||
"https://" + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git",
|
||||
},
|
||||
{
|
||||
"https://user:password@github.com/go-gitea/test_repo.git",
|
||||
"https://" + userPlaceholder + "@github.com/go-gitea/test_repo.git",
|
||||
"https://" + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git",
|
||||
},
|
||||
{
|
||||
"https://user:password@[::]/go-gitea/test_repo.git",
|
||||
"https://" + userInfoPlaceholder + "@[::]/go-gitea/test_repo.git",
|
||||
},
|
||||
{
|
||||
"https://user:password@[2001:db8::1]:8080/go-gitea/test_repo.git",
|
||||
"https://" + userInfoPlaceholder + "@[2001:db8::1]:8080/go-gitea/test_repo.git",
|
||||
},
|
||||
{
|
||||
"see https://u:p@[::1]/x and https://u2:p2@h2",
|
||||
"see https://" + userInfoPlaceholder + "@[::1]/x and https://" + userInfoPlaceholder + "@h2",
|
||||
},
|
||||
{
|
||||
"https://user:secret@[unclosed-ipv6",
|
||||
"https://user:secret@[unclosed-ipv6",
|
||||
},
|
||||
{
|
||||
"https://user:secret@[invalid-ipv6]",
|
||||
"https://user:secret@[invalid-ipv6]",
|
||||
},
|
||||
{
|
||||
"ftp://x@",
|
||||
"ftp://" + userPlaceholder + "@",
|
||||
"ftp://x@",
|
||||
},
|
||||
{
|
||||
"ftp://x/@",
|
||||
@@ -43,27 +63,40 @@ func TestSanitizeCredentialURLs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"ftp://u@x/@", // test multiple @ chars
|
||||
"ftp://" + userPlaceholder + "@x/@",
|
||||
"ftp://" + userInfoPlaceholder + "@x/@",
|
||||
},
|
||||
{
|
||||
"😊ftp://u@x😊", // test unicode
|
||||
"😊ftp://" + userPlaceholder + "@x😊",
|
||||
"😊ftp://" + userInfoPlaceholder + "@x😊",
|
||||
},
|
||||
{
|
||||
"://@",
|
||||
"://@",
|
||||
},
|
||||
{
|
||||
"//u:p@h", // do not process URLs without explicit scheme, they are not treated as "valid" URLs because there is no scheme context in string
|
||||
"//u:p@h",
|
||||
"//" + userInfoPlaceholder + "@h",
|
||||
},
|
||||
{
|
||||
"s://u@h", // the minimal pattern to be sanitized
|
||||
"s://" + userPlaceholder + "@h",
|
||||
"s://u@h",
|
||||
"s://" + userInfoPlaceholder + "@h",
|
||||
},
|
||||
{
|
||||
"URLs in log https://u:b@h and https://u:b@h:80/, with https://h.com and u@h.com",
|
||||
"URLs in log https://" + userPlaceholder + "@h and https://" + userPlaceholder + "@h:80/, with https://h.com and u@h.com",
|
||||
"URLs in log https://" + userInfoPlaceholder + "@h and https://" + userInfoPlaceholder + "@h:80/, with https://h.com and u@h.com",
|
||||
},
|
||||
{
|
||||
"fatal: unable to look up username:token@github.com (port 9418)",
|
||||
"fatal: unable to look up " + userInfoPlaceholder + "@github.com (port 9418)",
|
||||
},
|
||||
{
|
||||
"git failed for user:token@github.com/go-gitea/test_repo.git",
|
||||
"git failed for " + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git",
|
||||
},
|
||||
{
|
||||
// SSH-form git URL ("git@host:path") must not let a later credential URL through
|
||||
"failed remote git@github.com:foo, retried via https://user:tok@github.com/foo",
|
||||
"failed remote git@github.com:foo, retried via https://" + userInfoPlaceholder + "@github.com/foo",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -12,19 +12,19 @@ import (
|
||||
)
|
||||
|
||||
func TestDebounce(t *testing.T) {
|
||||
var c int64
|
||||
var c atomic.Int64
|
||||
d := Debounce(50 * time.Millisecond)
|
||||
d(func() { atomic.AddInt64(&c, 1) })
|
||||
assert.EqualValues(t, 0, atomic.LoadInt64(&c))
|
||||
d(func() { atomic.AddInt64(&c, 1) })
|
||||
d(func() { atomic.AddInt64(&c, 1) })
|
||||
d(func() { c.Add(1) })
|
||||
assert.EqualValues(t, 0, c.Load())
|
||||
d(func() { c.Add(1) })
|
||||
d(func() { c.Add(1) })
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
assert.EqualValues(t, 1, atomic.LoadInt64(&c))
|
||||
d(func() { atomic.AddInt64(&c, 1) })
|
||||
assert.EqualValues(t, 1, atomic.LoadInt64(&c))
|
||||
d(func() { atomic.AddInt64(&c, 1) })
|
||||
d(func() { atomic.AddInt64(&c, 1) })
|
||||
d(func() { atomic.AddInt64(&c, 1) })
|
||||
assert.EqualValues(t, 1, c.Load())
|
||||
d(func() { c.Add(1) })
|
||||
assert.EqualValues(t, 1, c.Load())
|
||||
d(func() { c.Add(1) })
|
||||
d(func() { c.Add(1) })
|
||||
d(func() { c.Add(1) })
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
assert.EqualValues(t, 2, atomic.LoadInt64(&c))
|
||||
assert.EqualValues(t, 2, c.Load())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package util
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -19,27 +18,6 @@ func PathEscapeSegments(path string) string {
|
||||
return escapedPath
|
||||
}
|
||||
|
||||
// URLJoin joins url components, like path.Join, but preserving contents
|
||||
func URLJoin(base string, elems ...string) string {
|
||||
if !strings.HasSuffix(base, "/") {
|
||||
base += "/"
|
||||
}
|
||||
baseURL, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
joinedPath := path.Join(elems...)
|
||||
argURL, err := url.Parse(joinedPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
joinedURL := baseURL.ResolveReference(argURL).String()
|
||||
if !baseURL.IsAbs() && !strings.HasPrefix(base, "/") {
|
||||
return joinedURL[1:] // Removing leading '/' if needed
|
||||
}
|
||||
return joinedURL
|
||||
}
|
||||
|
||||
func SanitizeURL(s string) (string, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -90,12 +91,12 @@ func CryptoRandomBytes(length int64) ([]byte, error) {
|
||||
return buf, err
|
||||
}
|
||||
|
||||
// ToUpperASCII returns s with all ASCII letters mapped to their upper case.
|
||||
func ToUpperASCII(s string) string {
|
||||
// ToLowerASCII returns s with all ASCII letters mapped to their lower case.
|
||||
func ToLowerASCII(s string) string {
|
||||
b := []byte(s)
|
||||
for i, c := range b {
|
||||
if 'a' <= c && c <= 'z' {
|
||||
b[i] -= 'a' - 'A'
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
b[i] += 'a' - 'A'
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
@@ -197,11 +198,6 @@ func ToFloat64(number any) (float64, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// ToPointer returns the pointer of a copy of any given value
|
||||
func ToPointer[T any](val T) *T {
|
||||
return &val
|
||||
}
|
||||
|
||||
// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
|
||||
func Iif[T any](condition bool, trueVal, falseVal T) T {
|
||||
if condition {
|
||||
@@ -245,11 +241,27 @@ func OptionalArg[T any](optArg []T, defaultValue ...T) (ret T) {
|
||||
return ret
|
||||
}
|
||||
|
||||
func ReserveLineBreakForTextarea(input string) string {
|
||||
type EnumConst[T comparable] interface {
|
||||
EnumValues() []T
|
||||
}
|
||||
|
||||
// EnumValue returns the value if it's in the enum const's values,
|
||||
// otherwise returns the first item of enums as default value.
|
||||
func EnumValue[T comparable](val EnumConst[T]) (ret T, valid bool) {
|
||||
enums := val.EnumValues()
|
||||
if slices.Contains(enums, val.(T)) {
|
||||
return val.(T), true
|
||||
}
|
||||
return enums[0], false
|
||||
}
|
||||
|
||||
func NormalizeStringEOL(input string) string {
|
||||
// Since the content is from a form which is a textarea, the line endings are \r\n.
|
||||
// It's a standard behavior of HTML.
|
||||
// But we want to store them as \n like what GitHub does.
|
||||
// And users are unlikely to really need to keep the \r.
|
||||
// But in most cases, we only want "\n" for EOL
|
||||
// * Text files: use "\n" by default because "\r\n" sometimes doesn't work in POSIX
|
||||
// * Actions values: store them as "\n" like what GitHub does.
|
||||
// And users are unlikely to really need the "\r".
|
||||
// Other than this, we should respect the original content, even leading or trailing spaces.
|
||||
return strings.ReplaceAll(input, "\r\n", "\n")
|
||||
return UnsafeBytesToString(NormalizeEOL(UnsafeStringToBytes(input)))
|
||||
}
|
||||
|
||||
@@ -11,39 +11,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestURLJoin(t *testing.T) {
|
||||
type test struct {
|
||||
Expected string
|
||||
Base string
|
||||
Elements []string
|
||||
}
|
||||
newTest := func(expected, base string, elements ...string) test {
|
||||
return test{Expected: expected, Base: base, Elements: elements}
|
||||
}
|
||||
for _, test := range []test{
|
||||
newTest("https://try.gitea.io/a/b/c",
|
||||
"https://try.gitea.io", "a/b", "c"),
|
||||
newTest("https://try.gitea.io/a/b/c",
|
||||
"https://try.gitea.io/", "/a/b/", "/c/"),
|
||||
newTest("https://try.gitea.io/a/c",
|
||||
"https://try.gitea.io/", "/a/./b/", "../c/"),
|
||||
newTest("a/b/c",
|
||||
"a", "b/c/"),
|
||||
newTest("a/b/d",
|
||||
"a/", "b/c/", "/../d/"),
|
||||
newTest("https://try.gitea.io/a/b/c#d",
|
||||
"https://try.gitea.io", "a/b", "c#d"),
|
||||
newTest("/a/b/d",
|
||||
"/a/", "b/c/", "/../d/"),
|
||||
newTest("/a/b/c",
|
||||
"/a", "b/c/"),
|
||||
newTest("/a/b/c#hash",
|
||||
"/a", "b/c#hash"),
|
||||
} {
|
||||
assert.Equal(t, test.Expected, URLJoin(test.Base, test.Elements...))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsEmptyString(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
@@ -178,30 +145,26 @@ type StringTest struct {
|
||||
in, out string
|
||||
}
|
||||
|
||||
var upperTests = []StringTest{
|
||||
var lowerTests = []StringTest{
|
||||
{"", ""},
|
||||
{"ONLYUPPER", "ONLYUPPER"},
|
||||
{"abc", "ABC"},
|
||||
{"AbC123", "ABC123"},
|
||||
{"azAZ09_", "AZAZ09_"},
|
||||
{"longStrinGwitHmixofsmaLLandcAps", "LONGSTRINGWITHMIXOFSMALLANDCAPS"},
|
||||
{"long\u0250string\u0250with\u0250nonascii\u2C6Fchars", "LONG\u0250STRING\u0250WITH\u0250NONASCII\u2C6FCHARS"},
|
||||
{"\u0250\u0250\u0250\u0250\u0250", "\u0250\u0250\u0250\u0250\u0250"},
|
||||
{"a\u0080\U0010FFFF", "A\u0080\U0010FFFF"},
|
||||
{"lél", "LéL"},
|
||||
{"ABC", "abc"},
|
||||
{"AbC123_", "abc123_"},
|
||||
{"LONG\u0250string\u0250WITH\u0250non-ascii\u2C6FCHARS\u0080\uFFFF", "long\u0250string\u0250with\u0250non-ascii\u2C6Fchars\u0080\uFFFF"},
|
||||
{"lél", "lél"},
|
||||
{"LÉL", "lÉl"},
|
||||
}
|
||||
|
||||
func TestToUpperASCII(t *testing.T) {
|
||||
for _, tc := range upperTests {
|
||||
assert.Equal(t, ToUpperASCII(tc.in), tc.out)
|
||||
func TestToLowerASCII(t *testing.T) {
|
||||
for _, tc := range lowerTests {
|
||||
assert.Equal(t, ToLowerASCII(tc.in), tc.out)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToUpper(b *testing.B) {
|
||||
for _, tc := range upperTests {
|
||||
func BenchmarkToLower(b *testing.B) {
|
||||
for _, tc := range lowerTests {
|
||||
b.Run(tc.in, func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
ToUpperASCII(tc.in)
|
||||
ToLowerASCII(tc.in)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -212,18 +175,9 @@ func TestToTitleCase(t *testing.T) {
|
||||
assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`FOO BAR BAZ`))
|
||||
}
|
||||
|
||||
func TestToPointer(t *testing.T) {
|
||||
assert.Equal(t, "abc", *ToPointer("abc"))
|
||||
assert.Equal(t, 123, *ToPointer(123))
|
||||
abc := "abc"
|
||||
assert.NotSame(t, &abc, ToPointer(abc))
|
||||
val123 := 123
|
||||
assert.NotSame(t, &val123, ToPointer(val123))
|
||||
}
|
||||
|
||||
func TestReserveLineBreakForTextarea(t *testing.T) {
|
||||
assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata"))
|
||||
assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n"))
|
||||
func TestNormalizeStringEOL(t *testing.T) {
|
||||
assert.Equal(t, "test\ndata", NormalizeStringEOL("test\r\ndata"))
|
||||
assert.Equal(t, " test\ndata\n ", NormalizeStringEOL(" test\rdata\r "))
|
||||
}
|
||||
|
||||
func TestOptionalArg(t *testing.T) {
|
||||
@@ -238,3 +192,10 @@ func TestOptionalArg(t *testing.T) {
|
||||
assert.Equal(t, 42, bar(nil))
|
||||
assert.Equal(t, 100, bar(nil, 100))
|
||||
}
|
||||
|
||||
func TestPathEscapeSegments(t *testing.T) {
|
||||
assert.Equal(t, "a", PathEscapeSegments("a"))
|
||||
assert.Equal(t, "a/b", PathEscapeSegments("a/b"))
|
||||
assert.Equal(t, "a/b%20c", PathEscapeSegments("a/b c"))
|
||||
assert.Equal(t, "a/b+c", PathEscapeSegments("a/b+c"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user